docx-generator/DocumentWizard.cpp
2026-04-29 07:15:22 +05:00

372 lines
14 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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);
}