init
This commit is contained in:
commit
6dafd0eca8
371
DocumentWizard.cpp
Normal file
371
DocumentWizard.cpp
Normal file
@ -0,0 +1,371 @@
|
||||
#include "DocumentWizard.h"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QListWidget>
|
||||
#include <QPushButton>
|
||||
#include <QLineEdit>
|
||||
#include <QComboBox>
|
||||
#include <QSpinBox>
|
||||
#include <QStackedWidget>
|
||||
#include <QGroupBox>
|
||||
#include <QFileDialog>
|
||||
#include <QMessageBox>
|
||||
#include <QFrame>
|
||||
#include <QFont>
|
||||
#include <QScrollArea>
|
||||
|
||||
// ============================================================
|
||||
// Парсинг шаблонных строк
|
||||
//
|
||||
// Формат строки:
|
||||
// heading1|Введите название раздела|Вариант А;Вариант Б;Вариант В
|
||||
// text|Выберите описание|Короткое;Подробное
|
||||
// table|Заполните таблицу характеристик|
|
||||
// image|Вставьте изображение схемы|
|
||||
//
|
||||
// Если choices пуст — пользователь вводит текст вручную.
|
||||
// ============================================================
|
||||
|
||||
DocumentWizard::DocumentWizard(const QStringList& templateLines, QWidget* parent)
|
||||
: QDialog(parent)
|
||||
{
|
||||
setWindowTitle("Мастер создания документа");
|
||||
setMinimumSize(640, 520);
|
||||
setupUi();
|
||||
parseTemplates(templateLines);
|
||||
|
||||
if (!m_steps.isEmpty())
|
||||
showStep(0);
|
||||
}
|
||||
|
||||
void DocumentWizard::parseTemplates(const QStringList& lines) {
|
||||
m_steps.clear();
|
||||
|
||||
static const QMap<QString, DocElementType> typeMap = {
|
||||
{"heading1", DocElementType::Heading1},
|
||||
{"heading2", DocElementType::Heading2},
|
||||
{"heading3", DocElementType::Heading3},
|
||||
{"text", DocElementType::NormalText},
|
||||
{"table", DocElementType::Table},
|
||||
{"image", DocElementType::Image},
|
||||
};
|
||||
|
||||
for (const QString& rawLine : lines) {
|
||||
QString line = rawLine.trimmed();
|
||||
if (line.isEmpty() || line.startsWith('#'))
|
||||
continue;
|
||||
|
||||
QStringList parts = line.split('|');
|
||||
if (parts.size() < 2)
|
||||
continue;
|
||||
|
||||
WizardStep step;
|
||||
step.elementType = typeMap.value(parts[0].trimmed().toLower(),
|
||||
DocElementType::NormalText);
|
||||
step.prompt = parts[1].trimmed();
|
||||
|
||||
if (parts.size() >= 3 && !parts[2].trimmed().isEmpty()) {
|
||||
for (const QString& ch : parts[2].split(';'))
|
||||
step.choices << ch.trimmed();
|
||||
}
|
||||
|
||||
m_steps.append(step);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// UI
|
||||
// ============================================================
|
||||
|
||||
void DocumentWizard::setupUi() {
|
||||
auto* mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setSpacing(12);
|
||||
mainLayout->setContentsMargins(16, 16, 16, 16);
|
||||
|
||||
// --- Заголовок шага ---
|
||||
m_stepLabel = new QLabel(this);
|
||||
QFont stepFont = m_stepLabel->font();
|
||||
stepFont.setBold(true);
|
||||
stepFont.setPointSize(11);
|
||||
m_stepLabel->setFont(stepFont);
|
||||
m_stepLabel->setStyleSheet("color: #2c5f8a;");
|
||||
mainLayout->addWidget(m_stepLabel);
|
||||
|
||||
// --- Подсказка ---
|
||||
m_promptLabel = new QLabel(this);
|
||||
m_promptLabel->setWordWrap(true);
|
||||
m_promptLabel->setStyleSheet("background:#f0f4f8; padding:8px; border-radius:4px;");
|
||||
mainLayout->addWidget(m_promptLabel);
|
||||
|
||||
// --- Стек страниц контента ---
|
||||
m_contentStack = new QStackedWidget(this);
|
||||
mainLayout->addWidget(m_contentStack, 1);
|
||||
|
||||
// -- Страница 0: список вариантов + свободный ввод --
|
||||
auto* choicePage = new QWidget;
|
||||
auto* choiceLayout = new QVBoxLayout(choicePage);
|
||||
choiceLayout->setContentsMargins(0,0,0,0);
|
||||
|
||||
m_choiceList = new QListWidget;
|
||||
m_choiceList->setAlternatingRowColors(true);
|
||||
choiceLayout->addWidget(new QLabel("Выберите вариант:"));
|
||||
choiceLayout->addWidget(m_choiceList);
|
||||
choiceLayout->addWidget(new QLabel("Или введите свой текст:"));
|
||||
m_freeInput = new QLineEdit;
|
||||
m_freeInput->setPlaceholderText("Введите текст вручную…");
|
||||
choiceLayout->addWidget(m_freeInput);
|
||||
m_contentStack->addWidget(choicePage); // index 0
|
||||
|
||||
// -- Страница 1: таблица --
|
||||
auto* tablePage = new QWidget;
|
||||
auto* tableLayout = new QVBoxLayout(tablePage);
|
||||
tableLayout->setContentsMargins(0,0,0,0);
|
||||
|
||||
tableLayout->addWidget(new QLabel("Заголовки столбцов (через ';'):"));
|
||||
m_tableHeadersEdit = new QLineEdit;
|
||||
m_tableHeadersEdit->setPlaceholderText("Параметр;Значение;Описание");
|
||||
tableLayout->addWidget(m_tableHeadersEdit);
|
||||
|
||||
tableLayout->addWidget(new QLabel("Строки (добавьте каждую строку; значения через ';'):"));
|
||||
m_tableRowsList = new QListWidget;
|
||||
tableLayout->addWidget(m_tableRowsList, 1);
|
||||
|
||||
auto* addRowLayout = new QHBoxLayout;
|
||||
m_tableRowInput = new QLineEdit;
|
||||
m_tableRowInput->setPlaceholderText("Значение1;Значение2;Значение3");
|
||||
m_addRowBtn = new QPushButton("+ Добавить строку");
|
||||
m_removeRowBtn = new QPushButton("✕ Удалить");
|
||||
addRowLayout->addWidget(m_tableRowInput, 1);
|
||||
addRowLayout->addWidget(m_addRowBtn);
|
||||
addRowLayout->addWidget(m_removeRowBtn);
|
||||
tableLayout->addLayout(addRowLayout);
|
||||
m_contentStack->addWidget(tablePage); // index 1
|
||||
|
||||
// -- Страница 2: изображение --
|
||||
auto* imgPage = new QWidget;
|
||||
auto* imgLayout = new QVBoxLayout(imgPage);
|
||||
imgLayout->setContentsMargins(0,0,0,0);
|
||||
|
||||
imgLayout->addWidget(new QLabel("Путь к изображению:"));
|
||||
auto* imgRowLayout = new QHBoxLayout;
|
||||
m_imagePathEdit = new QLineEdit;
|
||||
m_imagePathEdit->setPlaceholderText("/путь/к/файлу.png");
|
||||
m_browseBtn = new QPushButton("Обзор…");
|
||||
imgRowLayout->addWidget(m_imagePathEdit, 1);
|
||||
imgRowLayout->addWidget(m_browseBtn);
|
||||
imgLayout->addLayout(imgRowLayout);
|
||||
|
||||
auto* dimLayout = new QHBoxLayout;
|
||||
dimLayout->addWidget(new QLabel("Ширина (см):"));
|
||||
m_imgWidthCm = new QSpinBox; m_imgWidthCm->setRange(1, 30); m_imgWidthCm->setValue(10);
|
||||
dimLayout->addWidget(m_imgWidthCm);
|
||||
dimLayout->addWidget(new QLabel("Высота (см):"));
|
||||
m_imgHeightCm = new QSpinBox; m_imgHeightCm->setRange(1, 30); m_imgHeightCm->setValue(7);
|
||||
dimLayout->addWidget(m_imgHeightCm);
|
||||
dimLayout->addStretch();
|
||||
imgLayout->addLayout(dimLayout);
|
||||
imgLayout->addStretch();
|
||||
m_contentStack->addWidget(imgPage); // index 2
|
||||
|
||||
// --- Прогресс ---
|
||||
m_progressLabel = new QLabel(this);
|
||||
m_progressLabel->setAlignment(Qt::AlignRight);
|
||||
m_progressLabel->setStyleSheet("color: #888;");
|
||||
mainLayout->addWidget(m_progressLabel);
|
||||
|
||||
// --- Кнопки навигации ---
|
||||
auto* btnLayout = new QHBoxLayout;
|
||||
m_backBtn = new QPushButton("◀ Назад");
|
||||
m_nextBtn = new QPushButton("Далее ▶");
|
||||
m_finishBtn = new QPushButton("💾 Сохранить документ");
|
||||
m_finishBtn->setStyleSheet("background:#2c5f8a; color:white; font-weight:bold; padding:6px 18px;");
|
||||
m_finishBtn->setVisible(false);
|
||||
|
||||
btnLayout->addWidget(m_backBtn);
|
||||
btnLayout->addStretch();
|
||||
btnLayout->addWidget(m_nextBtn);
|
||||
btnLayout->addWidget(m_finishBtn);
|
||||
mainLayout->addLayout(btnLayout);
|
||||
|
||||
// --- Соединения ---
|
||||
connect(m_choiceList, &QListWidget::currentRowChanged,
|
||||
this, &DocumentWizard::onChoiceSelected);
|
||||
connect(m_addRowBtn, &QPushButton::clicked, this, &DocumentWizard::onAddTableRow);
|
||||
connect(m_removeRowBtn, &QPushButton::clicked, this, &DocumentWizard::onRemoveTableRow);
|
||||
connect(m_browseBtn, &QPushButton::clicked, this, &DocumentWizard::onBrowseImage);
|
||||
connect(m_backBtn, &QPushButton::clicked, this, &DocumentWizard::onBackClicked);
|
||||
connect(m_nextBtn, &QPushButton::clicked, this, &DocumentWizard::onNextClicked);
|
||||
connect(m_finishBtn, &QPushButton::clicked, this, &DocumentWizard::onFinishClicked);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Отображение шага
|
||||
// ============================================================
|
||||
|
||||
void DocumentWizard::showStep(int index) {
|
||||
if (index < 0 || index >= m_steps.size()) return;
|
||||
m_currentStep = index;
|
||||
|
||||
const WizardStep& step = m_steps[index];
|
||||
|
||||
m_stepLabel->setText(QString("Шаг %1 из %2").arg(index + 1).arg(m_steps.size()));
|
||||
m_promptLabel->setText(step.prompt);
|
||||
m_progressLabel->setText(QString("%1 / %2 шагов выполнено").arg(index).arg(m_steps.size()));
|
||||
|
||||
m_backBtn->setEnabled(index > 0);
|
||||
bool isLast = (index == m_steps.size() - 1);
|
||||
m_nextBtn->setVisible(!isLast);
|
||||
m_finishBtn->setVisible(isLast);
|
||||
|
||||
// Настраиваем страницу в зависимости от типа
|
||||
if (step.elementType == DocElementType::Table) {
|
||||
m_contentStack->setCurrentIndex(1);
|
||||
m_tableRowsList->clear();
|
||||
m_tableHeadersEdit->clear();
|
||||
m_tableRowInput->clear();
|
||||
} else if (step.elementType == DocElementType::Image) {
|
||||
m_contentStack->setCurrentIndex(2);
|
||||
m_imagePathEdit->clear();
|
||||
} else {
|
||||
m_contentStack->setCurrentIndex(0);
|
||||
m_choiceList->clear();
|
||||
m_freeInput->clear();
|
||||
for (const QString& ch : step.choices)
|
||||
m_choiceList->addItem(ch);
|
||||
// Если вариантов нет — скрываем список
|
||||
m_choiceList->setVisible(!step.choices.isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Сбор данных текущего шага и добавление в DocxBuilder
|
||||
// ============================================================
|
||||
|
||||
void DocumentWizard::applyCurrentStep() {
|
||||
const WizardStep& step = m_steps[m_currentStep];
|
||||
|
||||
switch (step.elementType) {
|
||||
case DocElementType::Heading1:
|
||||
case DocElementType::Heading2:
|
||||
case DocElementType::Heading3: {
|
||||
QString text = m_freeInput->text().trimmed();
|
||||
if (text.isEmpty() && m_choiceList->currentItem())
|
||||
text = m_choiceList->currentItem()->text();
|
||||
if (text.isEmpty()) {
|
||||
QMessageBox::warning(this, "Пусто", "Пожалуйста, введите или выберите текст заголовка.");
|
||||
return;
|
||||
}
|
||||
int level = (step.elementType == DocElementType::Heading1) ? 1
|
||||
: (step.elementType == DocElementType::Heading2) ? 2 : 3;
|
||||
m_builder.addHeading(text, level);
|
||||
break;
|
||||
}
|
||||
case DocElementType::NormalText: {
|
||||
QString text = m_freeInput->text().trimmed();
|
||||
if (text.isEmpty() && m_choiceList->currentItem())
|
||||
text = m_choiceList->currentItem()->text();
|
||||
if (text.isEmpty()) {
|
||||
QMessageBox::warning(this, "Пусто", "Пожалуйста, введите или выберите текст.");
|
||||
return;
|
||||
}
|
||||
DocStyle style;
|
||||
style.font = "Times New Roman";
|
||||
style.fontSize = 24; // 12pt
|
||||
m_builder.addParagraph(text, style);
|
||||
break;
|
||||
}
|
||||
case DocElementType::Table: {
|
||||
TableData td;
|
||||
QString hdr = m_tableHeadersEdit->text().trimmed();
|
||||
if (!hdr.isEmpty())
|
||||
td.headers = hdr.split(';');
|
||||
for (int i = 0; i < m_tableRowsList->count(); ++i)
|
||||
td.rows.append(m_tableRowsList->item(i)->text().split(';'));
|
||||
if (td.headers.isEmpty() && td.rows.isEmpty()) {
|
||||
QMessageBox::warning(this, "Таблица пуста", "Добавьте хотя бы одну строку в таблицу.");
|
||||
return;
|
||||
}
|
||||
m_builder.addTable(td);
|
||||
break;
|
||||
}
|
||||
case DocElementType::Image: {
|
||||
QString path = m_imagePathEdit->text().trimmed();
|
||||
if (path.isEmpty()) {
|
||||
QMessageBox::warning(this, "Нет файла", "Выберите изображение.");
|
||||
return;
|
||||
}
|
||||
// Конвертируем сантиметры в EMU (1 см = 360000 EMU)
|
||||
int wEmu = m_imgWidthCm->value() * 360000;
|
||||
int hEmu = m_imgHeightCm->value() * 360000;
|
||||
m_builder.addImage(path, wEmu, hEmu);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Слоты навигации
|
||||
// ============================================================
|
||||
|
||||
void DocumentWizard::onNextClicked() {
|
||||
applyCurrentStep();
|
||||
if (m_currentStep < m_steps.size() - 1)
|
||||
showStep(m_currentStep + 1);
|
||||
}
|
||||
|
||||
void DocumentWizard::onBackClicked() {
|
||||
if (m_currentStep > 0)
|
||||
showStep(m_currentStep - 1);
|
||||
}
|
||||
|
||||
void DocumentWizard::onFinishClicked() {
|
||||
applyCurrentStep();
|
||||
|
||||
QString path = QFileDialog::getSaveFileName(
|
||||
this, "Сохранить документ", "document.docx",
|
||||
"Word Documents (*.docx)");
|
||||
|
||||
if (path.isEmpty()) return;
|
||||
|
||||
if (m_builder.save(path)) {
|
||||
m_outputPath = path;
|
||||
QMessageBox::information(this, "Готово",
|
||||
QString("Документ успешно сохранён:\n%1").arg(path));
|
||||
accept();
|
||||
} else {
|
||||
QMessageBox::critical(this, "Ошибка",
|
||||
"Не удалось сохранить документ. Проверьте путь и права доступа.");
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Слоты работы с таблицей и изображением
|
||||
// ============================================================
|
||||
|
||||
void DocumentWizard::onChoiceSelected(int row) {
|
||||
if (row >= 0 && row < m_choiceList->count())
|
||||
m_freeInput->setText(m_choiceList->item(row)->text());
|
||||
}
|
||||
|
||||
void DocumentWizard::onAddTableRow() {
|
||||
QString rowText = m_tableRowInput->text().trimmed();
|
||||
if (!rowText.isEmpty()) {
|
||||
m_tableRowsList->addItem(rowText);
|
||||
m_tableRowInput->clear();
|
||||
}
|
||||
}
|
||||
|
||||
void DocumentWizard::onRemoveTableRow() {
|
||||
auto* item = m_tableRowsList->currentItem();
|
||||
if (item) delete item;
|
||||
}
|
||||
|
||||
void DocumentWizard::onBrowseImage() {
|
||||
QString path = QFileDialog::getOpenFileName(
|
||||
this, "Выбрать изображение", QString(),
|
||||
"Изображения (*.png *.jpg *.jpeg *.bmp)");
|
||||
if (!path.isEmpty())
|
||||
m_imagePathEdit->setText(path);
|
||||
}
|
||||
99
DocumentWizard.h
Normal file
99
DocumentWizard.h
Normal file
@ -0,0 +1,99 @@
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
#include <QList>
|
||||
#include <QStringList>
|
||||
#include "DocxBuilder.h"
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
class QLabel;
|
||||
class QListWidget;
|
||||
class QPushButton;
|
||||
class QLineEdit;
|
||||
class QComboBox;
|
||||
class QSpinBox;
|
||||
class QStackedWidget;
|
||||
class QGroupBox;
|
||||
QT_END_NAMESPACE
|
||||
|
||||
// ============================================================
|
||||
// Шаг мастера
|
||||
// ============================================================
|
||||
struct WizardStep {
|
||||
QString prompt; // Вопрос / подсказка пользователю
|
||||
QStringList choices; // Варианты выбора (если пусто — свободный ввод)
|
||||
DocElementType elementType; // Тип элемента, который создаём на этом шаге
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Диалог-мастер пошагового создания документа
|
||||
// ============================================================
|
||||
class DocumentWizard : public QDialog {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
// templateLines — набор строк-шаблонов, «скармливаемых» в начале работы.
|
||||
// Формат каждой строки: "type|prompt|choice1;choice2;choice3"
|
||||
// type: heading1 / heading2 / heading3 / text / table / image
|
||||
explicit DocumentWizard(const QStringList& templateLines,
|
||||
QWidget* parent = nullptr);
|
||||
|
||||
// Путь к созданному файлу
|
||||
QString outputFilePath() const { return m_outputPath; }
|
||||
|
||||
private slots:
|
||||
void onNextClicked();
|
||||
void onBackClicked();
|
||||
void onFinishClicked();
|
||||
void onChoiceSelected(int row);
|
||||
void onAddTableRow();
|
||||
void onRemoveTableRow();
|
||||
void onBrowseImage();
|
||||
|
||||
private:
|
||||
void parseTemplates(const QStringList& lines);
|
||||
void showStep(int index);
|
||||
void buildStepWidget(int index);
|
||||
void applyCurrentStep();
|
||||
|
||||
// --- Данные ---
|
||||
QList<WizardStep> m_steps;
|
||||
int m_currentStep = 0;
|
||||
QString m_outputPath;
|
||||
DocxBuilder m_builder;
|
||||
|
||||
// Промежуточные данные текущего шага
|
||||
QString m_chosenText;
|
||||
TableData m_tableData;
|
||||
QString m_imagePath;
|
||||
|
||||
// --- UI ---
|
||||
QLabel* m_stepLabel;
|
||||
QLabel* m_promptLabel;
|
||||
QStackedWidget* m_contentStack;
|
||||
|
||||
// Страница 0 — выбор из списка или ввод
|
||||
QListWidget* m_choiceList;
|
||||
QLineEdit* m_freeInput;
|
||||
|
||||
// Страница 1 — ввод таблицы
|
||||
QGroupBox* m_tableGroup;
|
||||
QLineEdit* m_tableHeadersEdit;
|
||||
QListWidget* m_tableRowsList;
|
||||
QLineEdit* m_tableRowInput;
|
||||
QPushButton* m_addRowBtn;
|
||||
QPushButton* m_removeRowBtn;
|
||||
|
||||
// Страница 2 — выбор изображения
|
||||
QLineEdit* m_imagePathEdit;
|
||||
QPushButton* m_browseBtn;
|
||||
QSpinBox* m_imgWidthCm;
|
||||
QSpinBox* m_imgHeightCm;
|
||||
|
||||
QPushButton* m_backBtn;
|
||||
QPushButton* m_nextBtn;
|
||||
QPushButton* m_finishBtn;
|
||||
QLabel* m_progressLabel;
|
||||
|
||||
void setupUi();
|
||||
};
|
||||
486
DocxBuilder.cpp
Normal file
486
DocxBuilder.cpp
Normal file
@ -0,0 +1,486 @@
|
||||
#include "DocxBuilder.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QBuffer>
|
||||
#include <QImage>
|
||||
#include <QByteArray>
|
||||
#include <QDir>
|
||||
#include <QDebug>
|
||||
|
||||
// Qt ZIP support (входит в состав Qt5 через QtCore private или quazip).
|
||||
// Здесь используем простую реализацию через QZipWriter (Qt >= 5.x, модуль QtCore/private).
|
||||
// Если QZipWriter недоступен — подключите QuaZip: https://github.com/stachenov/quazip
|
||||
#include <QtCore/private/qzipreader_p.h>
|
||||
#include <QtCore/private/qzipwriter_p.h>
|
||||
|
||||
// ============================================================
|
||||
DocxBuilder::DocxBuilder() = default;
|
||||
|
||||
void DocxBuilder::clear() {
|
||||
m_elements.clear();
|
||||
m_images.clear();
|
||||
m_imageCounter = 0;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Добавление элементов
|
||||
// ============================================================
|
||||
|
||||
void DocxBuilder::addHeading(const QString& text, int level) {
|
||||
DocElement el;
|
||||
el.text = text;
|
||||
switch (level) {
|
||||
case 1: el.type = DocElementType::Heading1; break;
|
||||
case 2: el.type = DocElementType::Heading2; break;
|
||||
default: el.type = DocElementType::Heading3; break;
|
||||
}
|
||||
el.style.font = "Times New Roman";
|
||||
el.style.bold = true;
|
||||
el.style.fontSize = (level == 1) ? 36 : (level == 2) ? 30 : 26;
|
||||
m_elements.append(el);
|
||||
}
|
||||
|
||||
void DocxBuilder::addParagraph(const QString& text, const DocStyle& style) {
|
||||
DocElement el;
|
||||
el.type = DocElementType::NormalText;
|
||||
el.text = text;
|
||||
el.style = style;
|
||||
m_elements.append(el);
|
||||
}
|
||||
|
||||
void DocxBuilder::addTable(const TableData& table) {
|
||||
DocElement el;
|
||||
el.type = DocElementType::Table;
|
||||
el.table = table;
|
||||
m_elements.append(el);
|
||||
}
|
||||
|
||||
void DocxBuilder::addImage(const QString& imagePath, int widthEmu, int heightEmu) {
|
||||
QFileInfo fi(imagePath);
|
||||
if (!fi.exists()) {
|
||||
qWarning() << "DocxBuilder::addImage — файл не найден:" << imagePath;
|
||||
return;
|
||||
}
|
||||
m_imageCounter++;
|
||||
ImageEntry entry;
|
||||
entry.sourcePath = imagePath;
|
||||
entry.ext = fi.suffix().toLower();
|
||||
entry.rId = QString("rId%1").arg(10 + m_imageCounter);
|
||||
entry.index = m_imageCounter;
|
||||
m_images.append(entry);
|
||||
|
||||
DocElement el;
|
||||
el.type = DocElementType::Image;
|
||||
el.imagePath = imagePath;
|
||||
el.imageWidthEmu = widthEmu;
|
||||
el.imageHeightEmu = heightEmu;
|
||||
// сохраняем индекс в fontSize (хак для передачи индекса)
|
||||
el.style.fontSize = m_imageCounter;
|
||||
m_elements.append(el);
|
||||
}
|
||||
|
||||
void DocxBuilder::addPageBreak() {
|
||||
DocElement el;
|
||||
el.type = DocElementType::NormalText;
|
||||
el.text = "\f"; // маркер разрыва страницы
|
||||
m_elements.append(el);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Сохранение
|
||||
// ============================================================
|
||||
|
||||
bool DocxBuilder::save(const QString& filePath) {
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::WriteOnly)) {
|
||||
qWarning() << "DocxBuilder::save — не удалось открыть файл:" << filePath;
|
||||
return false;
|
||||
}
|
||||
|
||||
QZipWriter zip(&file);
|
||||
zip.setCompressionPolicy(QZipWriter::AutoCompress);
|
||||
|
||||
// --- Обязательные части DOCX ---
|
||||
zip.addFile("[Content_Types].xml", buildContentTypesXml().toUtf8());
|
||||
zip.addFile("_rels/.rels", buildRelsXml().toUtf8());
|
||||
zip.addFile("word/document.xml", buildDocumentXml().toUtf8());
|
||||
zip.addFile("word/_rels/document.xml.rels", buildDocumentRelsXml().toUtf8());
|
||||
zip.addFile("word/styles.xml", buildStylesXml().toUtf8());
|
||||
zip.addFile("word/settings.xml", buildSettingsXml().toUtf8());
|
||||
|
||||
// --- Изображения ---
|
||||
for (const ImageEntry& img : m_images) {
|
||||
QFile imgFile(img.sourcePath);
|
||||
if (imgFile.open(QIODevice::ReadOnly)) {
|
||||
QByteArray data = imgFile.readAll();
|
||||
zip.addFile(QString("word/media/image%1.%2").arg(img.index).arg(img.ext), data);
|
||||
}
|
||||
}
|
||||
|
||||
zip.close();
|
||||
file.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Построение XML документа
|
||||
// ============================================================
|
||||
|
||||
QString DocxBuilder::buildDocumentXml() {
|
||||
QString body;
|
||||
|
||||
for (const DocElement& el : m_elements) {
|
||||
switch (el.type) {
|
||||
case DocElementType::Heading1:
|
||||
body += makeParagraph(el.text, el.style, "Heading1");
|
||||
break;
|
||||
case DocElementType::Heading2:
|
||||
body += makeParagraph(el.text, el.style, "Heading2");
|
||||
break;
|
||||
case DocElementType::Heading3:
|
||||
body += makeParagraph(el.text, el.style, "Heading3");
|
||||
break;
|
||||
case DocElementType::NormalText:
|
||||
if (el.text == "\f") {
|
||||
// Разрыв страницы
|
||||
body += R"(<w:p><w:r><w:br w:type="page"/></w:r></w:p>)";
|
||||
} else {
|
||||
body += makeParagraph(el.text, el.style);
|
||||
}
|
||||
break;
|
||||
case DocElementType::Table:
|
||||
body += makeTable(el.table);
|
||||
break;
|
||||
case DocElementType::Image: {
|
||||
// Найти соответствующую ImageEntry
|
||||
int idx = el.style.fontSize;
|
||||
ImageEntry* found = nullptr;
|
||||
for (ImageEntry& img : m_images) {
|
||||
if (img.index == idx) { found = &img; break; }
|
||||
}
|
||||
if (found) {
|
||||
QString imgXml = makeImageXml(
|
||||
found->rId.mid(3).toInt(),
|
||||
el.imageWidthEmu, el.imageHeightEmu, idx);
|
||||
body += QString("<w:p><w:r>%1</w:r></w:p>").arg(imgXml);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return QString(
|
||||
R"(<?xml version="1.0" encoding="UTF-8" standalone="yes"?>)"
|
||||
R"(<w:document xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas")"
|
||||
R"( xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main")"
|
||||
R"( xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships")"
|
||||
R"( xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing")"
|
||||
R"( xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main")"
|
||||
R"( xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">)"
|
||||
"<w:body>%1"
|
||||
R"(<w:sectPr><w:pgSz w:w="11906" w:h="16838"/><w:pgMar w:top="1134" w:right="850" w:bottom="1134" w:left="1701" w:header="709" w:footer="709" w:gutter="0"/></w:sectPr>)"
|
||||
"</w:body></w:document>"
|
||||
).arg(body);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Вспомогательные генераторы XML
|
||||
// ============================================================
|
||||
|
||||
QString DocxBuilder::escapeXml(const QString& text) {
|
||||
QString out = text;
|
||||
out.replace("&", "&");
|
||||
out.replace("<", "<");
|
||||
out.replace(">", ">");
|
||||
out.replace("\"", """);
|
||||
out.replace("'", "'");
|
||||
return out;
|
||||
}
|
||||
|
||||
QString DocxBuilder::makeRunProps(const DocStyle& style) {
|
||||
QString rpr = "<w:rPr>";
|
||||
rpr += QString("<w:rFonts w:ascii=\"%1\" w:hAnsi=\"%1\"/>").arg(style.font);
|
||||
rpr += QString("<w:sz w:val=\"%1\"/>").arg(style.fontSize);
|
||||
rpr += QString("<w:szCs w:val=\"%1\"/>").arg(style.fontSize);
|
||||
if (style.bold) rpr += "<w:b/><w:bCs/>";
|
||||
if (style.italic) rpr += "<w:i/><w:iCs/>";
|
||||
rpr += "</w:rPr>";
|
||||
return rpr;
|
||||
}
|
||||
|
||||
QString DocxBuilder::makeParaProps(const DocStyle& style, const QString& styleId) {
|
||||
QString ppr = "<w:pPr>";
|
||||
if (!styleId.isEmpty())
|
||||
ppr += QString("<w:pStyle w:val=\"%1\"/>").arg(styleId);
|
||||
if (style.center)
|
||||
ppr += "<w:jc w:val=\"center\"/>";
|
||||
// Отступы для обычного текста
|
||||
if (styleId.isEmpty())
|
||||
ppr += "<w:spacing w:line=\"276\" w:lineRule=\"auto\"/>";
|
||||
ppr += "</w:pPr>";
|
||||
return ppr;
|
||||
}
|
||||
|
||||
QString DocxBuilder::makeParagraph(const QString& text,
|
||||
const DocStyle& style,
|
||||
const QString& styleId)
|
||||
{
|
||||
// Разбиваем по переносам строки на отдельные параграфы
|
||||
QStringList lines = text.split('\n');
|
||||
QString result;
|
||||
for (int li = 0; li < lines.size(); ++li) {
|
||||
result += "<w:p>";
|
||||
result += makeParaProps(style, styleId);
|
||||
result += "<w:r>";
|
||||
result += makeRunProps(style);
|
||||
result += QString("<w:t xml:space=\"preserve\">%1</w:t>").arg(escapeXml(lines[li]));
|
||||
result += "</w:r>";
|
||||
result += "</w:p>";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
QString DocxBuilder::makeTable(const TableData& table) {
|
||||
// Считаем количество колонок
|
||||
int cols = table.headers.isEmpty()
|
||||
? (table.rows.isEmpty() ? 1 : table.rows[0].size())
|
||||
: table.headers.size();
|
||||
|
||||
// Ширина колонки: таблица на всю ширину страницы A4 (9356 DXA)
|
||||
int colWidth = (cols > 0) ? (9356 / cols) : 9356;
|
||||
|
||||
QString tbl;
|
||||
tbl += "<w:tbl>";
|
||||
|
||||
// Свойства таблицы
|
||||
tbl += "<w:tblPr>";
|
||||
tbl += "<w:tblStyle w:val=\"TableGrid\"/>";
|
||||
tbl += "<w:tblW w:w=\"9356\" w:type=\"dxa\"/>";
|
||||
tbl += "<w:tblBorders>"
|
||||
"<w:top w:val=\"single\" w:sz=\"4\" w:space=\"0\" w:color=\"000000\"/>"
|
||||
"<w:left w:val=\"single\" w:sz=\"4\" w:space=\"0\" w:color=\"000000\"/>"
|
||||
"<w:bottom w:val=\"single\" w:sz=\"4\" w:space=\"0\" w:color=\"000000\"/>"
|
||||
"<w:right w:val=\"single\" w:sz=\"4\" w:space=\"0\" w:color=\"000000\"/>"
|
||||
"<w:insideH w:val=\"single\" w:sz=\"4\" w:space=\"0\" w:color=\"000000\"/>"
|
||||
"<w:insideV w:val=\"single\" w:sz=\"4\" w:space=\"0\" w:color=\"000000\"/>"
|
||||
"</w:tblBorders>";
|
||||
tbl += "</w:tblPr>";
|
||||
|
||||
// Сетка колонок
|
||||
tbl += "<w:tblGrid>";
|
||||
for (int c = 0; c < cols; ++c)
|
||||
tbl += QString("<w:gridCol w:w=\"%1\"/>").arg(colWidth);
|
||||
tbl += "</w:tblGrid>";
|
||||
|
||||
// Строка заголовков
|
||||
if (!table.headers.isEmpty()) {
|
||||
tbl += "<w:tr>";
|
||||
for (const QString& hdr : table.headers) {
|
||||
tbl += "<w:tc>";
|
||||
tbl += QString("<w:tcPr><w:tcW w:w=\"%1\" w:type=\"dxa\"/>"
|
||||
"<w:shd w:val=\"clear\" w:color=\"auto\" w:fill=\"D9D9D9\"/>"
|
||||
"</w:tcPr>").arg(colWidth);
|
||||
tbl += "<w:p><w:pPr><w:jc w:val=\"center\"/></w:pPr>"
|
||||
"<w:r><w:rPr>"
|
||||
"<w:rFonts w:ascii=\"Times New Roman\" w:hAnsi=\"Times New Roman\"/>"
|
||||
"<w:b/><w:sz w:val=\"24\"/><w:szCs w:val=\"24\"/>"
|
||||
"</w:rPr>";
|
||||
tbl += QString("<w:t>%1</w:t></w:r></w:p>").arg(escapeXml(hdr));
|
||||
tbl += "</w:tc>";
|
||||
}
|
||||
tbl += "</w:tr>";
|
||||
}
|
||||
|
||||
// Строки данных
|
||||
for (const QStringList& row : table.rows) {
|
||||
tbl += "<w:tr>";
|
||||
for (int c = 0; c < cols; ++c) {
|
||||
QString cellText = (c < row.size()) ? row[c] : "";
|
||||
tbl += "<w:tc>";
|
||||
tbl += QString("<w:tcPr><w:tcW w:w=\"%1\" w:type=\"dxa\"/></w:tcPr>").arg(colWidth);
|
||||
tbl += "<w:p><w:r><w:rPr>"
|
||||
"<w:rFonts w:ascii=\"Times New Roman\" w:hAnsi=\"Times New Roman\"/>"
|
||||
"<w:sz w:val=\"24\"/><w:szCs w:val=\"24\"/>"
|
||||
"</w:rPr>";
|
||||
tbl += QString("<w:t xml:space=\"preserve\">%1</w:t></w:r></w:p>")
|
||||
.arg(escapeXml(cellText));
|
||||
tbl += "</w:tc>";
|
||||
}
|
||||
tbl += "</w:tr>";
|
||||
}
|
||||
|
||||
tbl += "</w:tbl>";
|
||||
// Пустой параграф после таблицы (обязателен по спецификации OOXML)
|
||||
tbl += "<w:p/>";
|
||||
return tbl;
|
||||
}
|
||||
|
||||
QString DocxBuilder::makeImageXml(int rId, int widthEmu, int heightEmu, int imgIdx) {
|
||||
// EMU: 914400 = 1 дюйм
|
||||
return QString(
|
||||
"<w:drawing>"
|
||||
"<wp:inline distT=\"0\" distB=\"0\" distL=\"0\" distR=\"0\">"
|
||||
"<wp:extent cx=\"%1\" cy=\"%2\"/>"
|
||||
"<wp:effectExtent l=\"0\" t=\"0\" r=\"0\" b=\"0\"/>"
|
||||
"<wp:docPr id=\"%3\" name=\"Image%3\"/>"
|
||||
"<wp:cNvGraphicFramePr>"
|
||||
"<a:graphicFrameLocks xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\""
|
||||
" noChangeAspect=\"1\"/>"
|
||||
"</wp:cNvGraphicFramePr>"
|
||||
"<a:graphic xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\">"
|
||||
"<a:graphicData uri=\"http://schemas.openxmlformats.org/drawingml/2006/picture\">"
|
||||
"<pic:pic xmlns:pic=\"http://schemas.openxmlformats.org/drawingml/2006/picture\">"
|
||||
"<pic:nvPicPr>"
|
||||
"<pic:cNvPr id=\"%3\" name=\"Image%3\"/>"
|
||||
"<pic:cNvPicPr/>"
|
||||
"</pic:nvPicPr>"
|
||||
"<pic:blipFill>"
|
||||
"<a:blip r:embed=\"rId%4\"/>"
|
||||
"<a:stretch><a:fillRect/></a:stretch>"
|
||||
"</pic:blipFill>"
|
||||
"<pic:spPr>"
|
||||
"<a:xfrm><a:off x=\"0\" y=\"0\"/><a:ext cx=\"%1\" cy=\"%2\"/></a:xfrm>"
|
||||
"<a:prstGeom prst=\"rect\"><a:avLst/></a:prstGeom>"
|
||||
"</pic:spPr>"
|
||||
"</pic:pic>"
|
||||
"</a:graphicData>"
|
||||
"</a:graphic>"
|
||||
"</wp:inline>"
|
||||
"</w:drawing>"
|
||||
).arg(widthEmu).arg(heightEmu).arg(imgIdx).arg(rId);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Системные XML-файлы DOCX
|
||||
// ============================================================
|
||||
|
||||
QString DocxBuilder::buildContentTypesXml() {
|
||||
QString ct =
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"
|
||||
"<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">"
|
||||
"<Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>"
|
||||
"<Default Extension=\"xml\" ContentType=\"application/xml\"/>"
|
||||
"<Override PartName=\"/word/document.xml\""
|
||||
" ContentType=\"application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml\"/>"
|
||||
"<Override PartName=\"/word/styles.xml\""
|
||||
" ContentType=\"application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml\"/>"
|
||||
"<Override PartName=\"/word/settings.xml\""
|
||||
" ContentType=\"application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml\"/>";
|
||||
|
||||
for (const ImageEntry& img : m_images) {
|
||||
QString mime = (img.ext == "png") ? "image/png" : "image/jpeg";
|
||||
ct += QString("<Default Extension=\"%1\" ContentType=\"%2\"/>").arg(img.ext).arg(mime);
|
||||
}
|
||||
|
||||
ct += "</Types>";
|
||||
return ct;
|
||||
}
|
||||
|
||||
QString DocxBuilder::buildRelsXml() {
|
||||
return
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"
|
||||
"<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">"
|
||||
"<Relationship Id=\"rId1\""
|
||||
" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument\""
|
||||
" Target=\"word/document.xml\"/>"
|
||||
"</Relationships>";
|
||||
}
|
||||
|
||||
QString DocxBuilder::buildDocumentRelsXml() {
|
||||
QString rels =
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"
|
||||
"<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">"
|
||||
"<Relationship Id=\"rId1\""
|
||||
" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles\""
|
||||
" Target=\"styles.xml\"/>"
|
||||
"<Relationship Id=\"rId2\""
|
||||
" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings\""
|
||||
" Target=\"settings.xml\"/>";
|
||||
|
||||
for (const ImageEntry& img : m_images) {
|
||||
rels += QString(
|
||||
"<Relationship Id=\"%1\""
|
||||
" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image\""
|
||||
" Target=\"media/image%2.%3\"/>"
|
||||
).arg(img.rId).arg(img.index).arg(img.ext);
|
||||
}
|
||||
|
||||
rels += "</Relationships>";
|
||||
return rels;
|
||||
}
|
||||
|
||||
QString DocxBuilder::buildSettingsXml() {
|
||||
return
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"
|
||||
"<w:settings xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\">"
|
||||
"<w:defaultTabStop w:val=\"720\"/>"
|
||||
"<w:compat><w:compatSetting w:name=\"compatibilityMode\" w:uri=\"http://schemas.microsoft.com/office/word\" w:val=\"15\"/></w:compat>"
|
||||
"</w:settings>";
|
||||
}
|
||||
|
||||
QString DocxBuilder::buildStylesXml() {
|
||||
// Определяем стили Times New Roman для заголовков и нормального текста
|
||||
return
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"
|
||||
"<w:styles xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\">"
|
||||
|
||||
// Normal
|
||||
"<w:style w:type=\"paragraph\" w:default=\"1\" w:styleId=\"Normal\">"
|
||||
"<w:name w:val=\"Normal\"/>"
|
||||
"<w:rPr>"
|
||||
"<w:rFonts w:ascii=\"Times New Roman\" w:hAnsi=\"Times New Roman\"/>"
|
||||
"<w:sz w:val=\"24\"/><w:szCs w:val=\"24\"/>"
|
||||
"</w:rPr>"
|
||||
"</w:style>"
|
||||
|
||||
// Heading 1 — Times New Roman 18pt Bold
|
||||
"<w:style w:type=\"paragraph\" w:styleId=\"Heading1\">"
|
||||
"<w:name w:val=\"heading 1\"/>"
|
||||
"<w:basedOn w:val=\"Normal\"/>"
|
||||
"<w:pPr><w:outlineLvl w:val=\"0\"/><w:spacing w:before=\"240\" w:after=\"120\"/></w:pPr>"
|
||||
"<w:rPr>"
|
||||
"<w:rFonts w:ascii=\"Times New Roman\" w:hAnsi=\"Times New Roman\"/>"
|
||||
"<w:b/><w:sz w:val=\"36\"/><w:szCs w:val=\"36\"/>"
|
||||
"</w:rPr>"
|
||||
"</w:style>"
|
||||
|
||||
// Heading 2 — Times New Roman 15pt Bold
|
||||
"<w:style w:type=\"paragraph\" w:styleId=\"Heading2\">"
|
||||
"<w:name w:val=\"heading 2\"/>"
|
||||
"<w:basedOn w:val=\"Normal\"/>"
|
||||
"<w:pPr><w:outlineLvl w:val=\"1\"/><w:spacing w:before=\"200\" w:after=\"100\"/></w:pPr>"
|
||||
"<w:rPr>"
|
||||
"<w:rFonts w:ascii=\"Times New Roman\" w:hAnsi=\"Times New Roman\"/>"
|
||||
"<w:b/><w:sz w:val=\"30\"/><w:szCs w:val=\"30\"/>"
|
||||
"</w:rPr>"
|
||||
"</w:style>"
|
||||
|
||||
// Heading 3 — Times New Roman 13pt Bold
|
||||
"<w:style w:type=\"paragraph\" w:styleId=\"Heading3\">"
|
||||
"<w:name w:val=\"heading 3\"/>"
|
||||
"<w:basedOn w:val=\"Normal\"/>"
|
||||
"<w:pPr><w:outlineLvl w:val=\"2\"/><w:spacing w:before=\"160\" w:after=\"80\"/></w:pPr>"
|
||||
"<w:rPr>"
|
||||
"<w:rFonts w:ascii=\"Times New Roman\" w:hAnsi=\"Times New Roman\"/>"
|
||||
"<w:b/><w:sz w:val=\"26\"/><w:szCs w:val=\"26\"/>"
|
||||
"</w:rPr>"
|
||||
"</w:style>"
|
||||
|
||||
// TableGrid
|
||||
"<w:style w:type=\"table\" w:styleId=\"TableGrid\">"
|
||||
"<w:name w:val=\"Table Grid\"/>"
|
||||
"<w:tblPr>"
|
||||
"<w:tblBorders>"
|
||||
"<w:top w:val=\"single\" w:sz=\"4\" w:space=\"0\" w:color=\"000000\"/>"
|
||||
"<w:left w:val=\"single\" w:sz=\"4\" w:space=\"0\" w:color=\"000000\"/>"
|
||||
"<w:bottom w:val=\"single\" w:sz=\"4\" w:space=\"0\" w:color=\"000000\"/>"
|
||||
"<w:right w:val=\"single\" w:sz=\"4\" w:space=\"0\" w:color=\"000000\"/>"
|
||||
"<w:insideH w:val=\"single\" w:sz=\"4\" w:space=\"0\" w:color=\"000000\"/>"
|
||||
"<w:insideV w:val=\"single\" w:sz=\"4\" w:space=\"0\" w:color=\"000000\"/>"
|
||||
"</w:tblBorders>"
|
||||
"</w:tblPr>"
|
||||
"</w:style>"
|
||||
|
||||
"</w:styles>";
|
||||
}
|
||||
98
DocxBuilder.h
Normal file
98
DocxBuilder.h
Normal file
@ -0,0 +1,98 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QList>
|
||||
#include <QByteArray>
|
||||
#include <QMap>
|
||||
|
||||
// ============================================================
|
||||
// Структуры данных для элементов документа
|
||||
// ============================================================
|
||||
|
||||
enum class DocElementType {
|
||||
Heading1,
|
||||
Heading2,
|
||||
Heading3,
|
||||
NormalText,
|
||||
Table,
|
||||
Image
|
||||
};
|
||||
|
||||
struct DocStyle {
|
||||
QString font = "Times New Roman";
|
||||
int fontSize = 24; // в половинах пунктов (24 = 12pt)
|
||||
bool bold = false;
|
||||
bool italic = false;
|
||||
bool center = false;
|
||||
};
|
||||
|
||||
struct TableData {
|
||||
QList<QStringList> rows; // rows[i][j] = текст ячейки
|
||||
QStringList headers;
|
||||
};
|
||||
|
||||
struct DocElement {
|
||||
DocElementType type;
|
||||
QString text; // для текста / заголовков
|
||||
DocStyle style;
|
||||
TableData table; // для таблиц
|
||||
QString imagePath; // для картинок
|
||||
int imageWidthEmu = 5400000; // ~6 см
|
||||
int imageHeightEmu = 3600000; // ~4 см
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Класс-строитель DOCX-документа
|
||||
// ============================================================
|
||||
|
||||
class DocxBuilder {
|
||||
public:
|
||||
DocxBuilder();
|
||||
|
||||
// --- Добавление элементов ---
|
||||
void addHeading(const QString& text, int level = 1);
|
||||
void addParagraph(const QString& text, const DocStyle& style = {});
|
||||
void addTable(const TableData& table);
|
||||
void addImage(const QString& imagePath,
|
||||
int widthEmu = 5400000,
|
||||
int heightEmu = 3600000);
|
||||
void addPageBreak();
|
||||
|
||||
// --- Сохранение ---
|
||||
bool save(const QString& filePath);
|
||||
|
||||
// --- Очистка ---
|
||||
void clear();
|
||||
|
||||
private:
|
||||
QList<DocElement> m_elements;
|
||||
int m_imageCounter = 0;
|
||||
|
||||
// Генерация XML-частей
|
||||
QString buildDocumentXml();
|
||||
QString buildContentTypesXml();
|
||||
QString buildRelsXml();
|
||||
QString buildDocumentRelsXml();
|
||||
QString buildSettingsXml();
|
||||
QString buildStylesXml();
|
||||
|
||||
// Вспомогательные методы генерации XML
|
||||
QString makeRunProps(const DocStyle& style);
|
||||
QString makeParaProps(const DocStyle& style, const QString& styleId = "");
|
||||
QString makeParagraph(const QString& text,
|
||||
const DocStyle& style,
|
||||
const QString& styleId = "");
|
||||
QString makeTable(const TableData& table);
|
||||
QString makeImageXml(int rId, int widthEmu, int heightEmu, int imgIdx);
|
||||
QString escapeXml(const QString& text);
|
||||
|
||||
// Для хранения изображений (путь → данные)
|
||||
struct ImageEntry {
|
||||
QString sourcePath;
|
||||
QString ext; // png, jpg, …
|
||||
QString rId;
|
||||
int index;
|
||||
};
|
||||
QList<ImageEntry> m_images;
|
||||
};
|
||||
37
DocxWizard.pro
Normal file
37
DocxWizard.pro
Normal file
@ -0,0 +1,37 @@
|
||||
QT += core gui widgets
|
||||
|
||||
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
|
||||
|
||||
CONFIG += c++17
|
||||
|
||||
TARGET = DocxWizard
|
||||
TEMPLATE = app
|
||||
|
||||
# Подключаем приватные заголовки Qt для QZipWriter/QZipReader
|
||||
# (входят в стандартную поставку Qt5)
|
||||
QT += core-private
|
||||
|
||||
SOURCES += \
|
||||
main.cpp \
|
||||
DocxBuilder.cpp \
|
||||
DocumentWizard.cpp
|
||||
|
||||
HEADERS += \
|
||||
DocxBuilder.h \
|
||||
DocumentWizard.h
|
||||
|
||||
# Если QZipWriter недоступен как private — используйте QuaZip:
|
||||
# Установка: sudo apt install libquazip5-dev (Linux)
|
||||
# или скачайте https://github.com/stachenov/quazip
|
||||
#
|
||||
# Тогда замените в DocxBuilder.cpp:
|
||||
# #include <QtCore/private/qzipreader_p.h>
|
||||
# #include <QtCore/private/qzipwriter_p.h>
|
||||
# на:
|
||||
# #include <quazip/quazip.h>
|
||||
# #include <quazip/quazipfile.h>
|
||||
# и адаптируйте метод save() под QuaZip API.
|
||||
#
|
||||
# Раскомментируйте, если используете QuaZip:
|
||||
# LIBS += -lquazip5
|
||||
# INCLUDEPATH += /usr/include/quazip5
|
||||
133
README.md
Normal file
133
README.md
Normal file
@ -0,0 +1,133 @@
|
||||
# DocxWizard — Модуль пошагового создания Word-документов
|
||||
|
||||
## Стек технологий
|
||||
|
||||
| Компонент | Инструмент | Зачем |
|
||||
|--------------------|-------------------------------|---------------------------------------------|
|
||||
| Язык | C++17 | Основной язык проекта |
|
||||
| GUI + логика | Qt5 (Widgets) | Уже используется в проекте |
|
||||
| Генерация DOCX | Qt5 (QZipWriter) или QuaZip | DOCX — это ZIP+XML; Qt умеет это нативно |
|
||||
| Изображения | Qt5 QFile | Чтение и вставка бинарных данных изображений |
|
||||
|
||||
**Внешних зависимостей нет** (кроме Qt5, которая уже есть).
|
||||
|
||||
---
|
||||
|
||||
## Архитектура
|
||||
|
||||
```
|
||||
main.cpp
|
||||
└── MainWindow ← Главное окно; редактор строк-шаблонов
|
||||
└── DocumentWizard ← Пошаговый диалог (QDialog)
|
||||
├── parseTemplates() ← Читает строки шаблона → список шагов
|
||||
├── showStep() ← Показывает нужный UI для шага
|
||||
├── applyCurrentStep() ← Передаёт данные в DocxBuilder
|
||||
└── DocxBuilder ← Генерирует .docx файл
|
||||
├── addHeading()
|
||||
├── addParagraph()
|
||||
├── addTable()
|
||||
├── addImage()
|
||||
└── save() ← Записывает ZIP/XML в файл
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Формат строк шаблона
|
||||
|
||||
Строки шаблона «скармливаются» программе в начале работы.
|
||||
Каждая строка задаёт один шаг мастера:
|
||||
|
||||
```
|
||||
тип|Подсказка для пользователя|Вариант1;Вариант2;Вариант3
|
||||
```
|
||||
|
||||
### Типы элементов
|
||||
|
||||
| Тип | Результат в документе | Примечание |
|
||||
|----------|-------------------------------|--------------------------------------|
|
||||
| heading1 | Заголовок 1 (18pt, жирный) | Times New Roman |
|
||||
| heading2 | Заголовок 2 (15pt, жирный) | Times New Roman |
|
||||
| heading3 | Заголовок 3 (13pt, жирный) | Times New Roman |
|
||||
| text | Обычный абзац (12pt) | Times New Roman |
|
||||
| table | Таблица с заголовками | UI для ввода строк |
|
||||
| image | Встроенное изображение | PNG/JPG, размер в см |
|
||||
|
||||
### Пример шаблона
|
||||
|
||||
```
|
||||
heading1|Введите название документа|Технический отчёт;Пояснительная записка
|
||||
heading2|Название раздела «Введение»|1. Введение;1. Общие сведения
|
||||
text|Напишите вводный абзац|Настоящий документ описывает…;В данной работе…
|
||||
table|Заполните таблицу характеристик|
|
||||
image|Вставьте схему архитектуры|
|
||||
heading2|Заключение|4. Заключение;4. Выводы
|
||||
text|Напишите заключение|В результате работы…;Таким образом…
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Сборка
|
||||
|
||||
```bash
|
||||
cd docx_module
|
||||
qmake DocxWizard.pro
|
||||
make # Linux/macOS
|
||||
# или
|
||||
nmake # Windows (MSVC)
|
||||
```
|
||||
|
||||
### Альтернатива QZipWriter → QuaZip
|
||||
|
||||
Если Qt5 собрана без приватных заголовков (`-private`):
|
||||
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo apt install libquazip5-dev
|
||||
|
||||
# или vcpkg
|
||||
vcpkg install quazip
|
||||
```
|
||||
|
||||
Затем в `DocxBuilder.cpp` замените:
|
||||
```cpp
|
||||
// Было:
|
||||
#include <QtCore/private/qzipwriter_p.h>
|
||||
|
||||
// Стало:
|
||||
#include <quazip/quazip.h>
|
||||
#include <quazip/quazipfile.h>
|
||||
```
|
||||
|
||||
И адаптируйте метод `DocxBuilder::save()` — используйте `QuaZip` + `QuaZipFile`
|
||||
вместо `QZipWriter`. API очень похожи.
|
||||
|
||||
---
|
||||
|
||||
## Интеграция в существующий Qt5 проект
|
||||
|
||||
1. Скопируйте `DocxBuilder.h/.cpp` и `DocumentWizard.h/.cpp` в свой проект.
|
||||
2. В `.pro` добавьте:
|
||||
```
|
||||
QT += core-private # для QZipWriter
|
||||
SOURCES += DocxBuilder.cpp DocumentWizard.cpp
|
||||
HEADERS += DocxBuilder.h DocumentWizard.h
|
||||
```
|
||||
3. Используйте из кода:
|
||||
```cpp
|
||||
QStringList tmpl = loadTemplateFromFile("template.txt");
|
||||
DocumentWizard dlg(tmpl, this);
|
||||
if (dlg.exec() == QDialog::Accepted)
|
||||
qDebug() << "Сохранено:" << dlg.outputFilePath();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Расширение функциональности
|
||||
|
||||
| Задача | Куда добавить |
|
||||
|--------------------------------|------------------------------------------|
|
||||
| Нумерованные списки | `DocxBuilder::addList()` |
|
||||
| Верхний/нижний колонтитул | `DocxBuilder::buildDocumentXml()` — `<w:sectPr>` |
|
||||
| Оглавление | XML-элемент `<w:fldChar>` с `TOC` |
|
||||
| Сохранение шаблона в файл | `QSettings` или простой `.txt` |
|
||||
| Предпросмотр документа | LibreOffice → конвертировать в PDF → показать |
|
||||
109
main.cpp
Normal file
109
main.cpp
Normal file
@ -0,0 +1,109 @@
|
||||
#include <QApplication>
|
||||
#include <QMainWindow>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
#include <QTextEdit>
|
||||
#include <QLabel>
|
||||
#include <QWidget>
|
||||
#include <QStringList>
|
||||
#include <QMessageBox>
|
||||
|
||||
#include "DocumentWizard.h"
|
||||
|
||||
// ============================================================
|
||||
// Пример главного окна приложения
|
||||
// ============================================================
|
||||
class MainWindow : public QMainWindow {
|
||||
Q_OBJECT
|
||||
public:
|
||||
MainWindow(QWidget* parent = nullptr) : QMainWindow(parent) {
|
||||
setWindowTitle("Генератор Word-документов");
|
||||
resize(600, 400);
|
||||
|
||||
auto* central = new QWidget(this);
|
||||
setCentralWidget(central);
|
||||
auto* layout = new QVBoxLayout(central);
|
||||
|
||||
auto* info = new QLabel(
|
||||
"<b>Шаблон документа</b> задаётся набором строк.<br>"
|
||||
"Каждая строка: <code>тип|подсказка|выбор1;выбор2</code><br>"
|
||||
"Типы: heading1, heading2, heading3, text, table, image",
|
||||
this);
|
||||
info->setWordWrap(true);
|
||||
info->setStyleSheet("background:#fffbe6; padding:10px; border-radius:4px;");
|
||||
layout->addWidget(info);
|
||||
|
||||
m_templateEdit = new QTextEdit(this);
|
||||
m_templateEdit->setPlaceholderText("Введите строки шаблона…");
|
||||
m_templateEdit->setPlainText(defaultTemplate());
|
||||
m_templateEdit->setFont(QFont("Courier New", 10));
|
||||
layout->addWidget(m_templateEdit, 1);
|
||||
|
||||
auto* startBtn = new QPushButton("▶ Запустить мастер создания документа", this);
|
||||
startBtn->setStyleSheet(
|
||||
"QPushButton { background:#2c5f8a; color:white; font-weight:bold;"
|
||||
" padding:10px; border-radius:6px; font-size:13px; }"
|
||||
"QPushButton:hover { background:#3a7ab5; }");
|
||||
layout->addWidget(startBtn);
|
||||
|
||||
connect(startBtn, &QPushButton::clicked, this, &MainWindow::launchWizard);
|
||||
}
|
||||
|
||||
private slots:
|
||||
void launchWizard() {
|
||||
QStringList lines = m_templateEdit->toPlainText().split('\n');
|
||||
DocumentWizard wizard(lines, this);
|
||||
wizard.exec();
|
||||
}
|
||||
|
||||
private:
|
||||
QTextEdit* m_templateEdit;
|
||||
|
||||
static QString defaultTemplate() {
|
||||
// Это и есть «строки, скармливаемые в начале работы программы»
|
||||
return
|
||||
"# Строки шаблона — определяют структуру будущего документа\n"
|
||||
"# Формат: тип|подсказка пользователю|вариант1;вариант2;вариант3\n"
|
||||
"\n"
|
||||
"heading1|Введите название документа (Заголовок 1)|"
|
||||
"Технический отчёт;Пояснительная записка;Руководство пользователя\n"
|
||||
"\n"
|
||||
"heading2|Выберите название раздела «Введение»|"
|
||||
"1. Введение;1. Общие сведения;1. Назначение системы\n"
|
||||
"\n"
|
||||
"text|Напишите вводный абзац|"
|
||||
"Настоящий документ описывает…;В данной работе рассматривается…;"
|
||||
"Целью настоящего документа является…\n"
|
||||
"\n"
|
||||
"heading2|Раздел «Характеристики»|"
|
||||
"2. Технические характеристики;2. Параметры системы\n"
|
||||
"\n"
|
||||
"table|Заполните таблицу характеристик системы|\n"
|
||||
"\n"
|
||||
"heading2|Раздел со схемой|"
|
||||
"3. Структурная схема;3. Архитектура решения\n"
|
||||
"\n"
|
||||
"image|Вставьте изображение схемы или диаграммы|\n"
|
||||
"\n"
|
||||
"heading2|Заключительный раздел|"
|
||||
"4. Заключение;4. Выводы\n"
|
||||
"\n"
|
||||
"text|Напишите заключение|"
|
||||
"В результате работы были получены…;Таким образом, в ходе…;"
|
||||
"Проведённые исследования показали…\n";
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// main
|
||||
// ============================================================
|
||||
int main(int argc, char* argv[]) {
|
||||
QApplication app(argc, argv);
|
||||
app.setStyle("Fusion");
|
||||
|
||||
MainWindow w;
|
||||
w.show();
|
||||
return app.exec();
|
||||
}
|
||||
|
||||
#include "main.moc"
|
||||
Loading…
Reference in New Issue
Block a user