This commit is contained in:
gre-ilya 2026-04-29 07:15:22 +05:00
commit 6dafd0eca8
7 changed files with 1333 additions and 0 deletions

371
DocumentWizard.cpp Normal file
View 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
View 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
View 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("&", "&amp;");
out.replace("<", "&lt;");
out.replace(">", "&gt;");
out.replace("\"", "&quot;");
out.replace("'", "&apos;");
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
View 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
View 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
View 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
View 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"