docx-generator/DocumentWizard.cpp

372 lines
14 KiB
C++
Raw Permalink Normal View History

2026-04-29 02:15:22 +00:00
#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);
}