This is my first attempt at using the QT portable graphic system. I need to learn QT to pursue some of the positions I am interested in.
I realize the photo size reducer problem may be getting boring, but when you're trying a whole new programming tool you want to solve problems you already understand.
Purpose and Goals
The primary motivation for this project is to learn as much about QT using C++ as I can. A secondary motivation was to create an easy to use GUI for the command line Photo Resize tool. A third goal was to create an Model View Controller using QT and C++, I was not successful in this third goal, this tool primarily uses the Model View ViewModel design pattern, there were just too many connections between Signals and Slots in the MVC design pattern. The would be controller does do a little bit of control, but the primary communication between the model and the views is direct.
There is an intermediate step between this GUI tool and the previous command line tool that was developed using QT Creator. I found I wasn't learning enough about developing QT C++ programs using QT Creator.
Features
- Rather than make the user type in the path, entering the directory line edits opens a file browser dialog.
- User errors are highlighted in yellow as soon as possible.
- All processing, including errors are in the Model.
The UI
The main window The main window, the user can only push buttons.
The options dialog
The options dialog, this is where the user enters all information.
Questions
- Is the code miss using the
QT Signals and Slots
feature? - During debugging there were some interesting side affects found that created recursion (loosing focus in the DirectoryLineEdit class). Is there any possible bugs in the communications caused by message windows or popup windows?
- Are there any QT best practices I am not following?
- Show the class destructors be declared as
default
in the header files? - Are there any problems with the code format, such as lines too long for the screen?
- Are there any suggestions on improving the user interface?
The Development Environment
- Ubuntu 22.04
- Visual Studio Code
- C++ 23
- gcc 12
- QT 6.8
- CMake 3.31
- openCV
The Code
All of the code for this project can be found in my GitHub Repository. This is a branch that will remain the same in the future, any additional development will be on the master. There is a previous branch that shows the code prior to subclassing the QLineEdit class. That version used buttons to open the file dialogues.
main.cpp
#include "SignalRouterController.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication theApp(argc, argv);
SignalRouterController routerController("TheController");
routerController.createModel();
routerController.connectControllerAndModelSignalsToSlots();
routerController.creatMainWindow();
routerController.connectModelAndMainWindowSignalsToSlots();
routerController.connectControllerAndMainWindowSignalsToSlots();
routerController.initMainWindowValuesAndShow();
return theApp.exec();
}
mainwindow.h
#ifndef MAINWINDOW_H_
#define MAINWINDOW_H_
#include <QVariant>
#include <QApplication>
#include <QHBoxLayout>
#include <QLCDNumber>
#include <QLabel>
#include <QLineEdit>
#include <QMainWindow>
#include <QMenuBar>
#include <QProgressBar>
#include <QPushButton>
#include <QRect>
#include <QStatusBar>
#include <QString>
#include <QVBoxLayout>
#include <QWidget>
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
public slots:
void on_resizedPhotos_valueChanged(std::size_t value);
void on_photosToResizeCount_ValueChanged(std::size_t photosToResize);
void on_SourceDirectory_Changed(QString srcDir);
void on_TargetDirectory_Changed(QString targetDir);
void enableResizePhotosButton() { resizePhotosButton->setEnabled(true); };
signals:
void mainWindowOptionsButtonPressed(bool doINeedSignalContents);
void resizeAllPhotos();
private slots:
void on_optionsPushButton_Clicked();
void on_resizePhotosButton_Clicked();
private:
void setUpMainWindowUI();
void setUpDirectoryDisplays();
void setUpProgressDisplays();
QLCDNumber* createAndConfigureLCD(const char* lcdName, const int initValue = 0);
QProgressBar* createAndConfigureProgressBar(const char* objectName, const int initValue = 0);
int getLabelWidth(QLabel* lab);
QLabel* createNamedLabel(const char* labText, const char* labName);
QLineEdit* createDirectoryDisplayLab(const char* labName);
QPushButton* CreateNamedButton(const char* buttonText, const char* buttonName);
QString generateWidthAndHeightStyleString(const int width, const int height);
/*
* Size and positioning constants.
*/
const int mainWindowWidth = 800;
const int mainWindowHeight = 500;
const int maxOjectWidth = static_cast<int>(mainWindowWidth * 0.8);
const int lcdHeight = 23;
const int lcdWidth = 200;
const int labelHeight = 17;
const int buttonHeight = 25;
const int progressBarHeight = 60;
const int lcdDigitCount = 5;
QWidget* centralwidget;
QVBoxLayout* mwLayout;
QHBoxLayout* photoCountHBoxLayout;
QVBoxLayout* filesToResizeVBoxLayout;
QLabel* filesToResizeLabel;
QLCDNumber* filesToResizeLcdNumber;
QVBoxLayout* resizePhotosVBoxLayout;
QLabel* resizedPhotosLabel;
QLCDNumber* resizedPhotosLcdNumber;
QLabel* sourceDirectoryLabel;
QProgressBar* resizeProgressBar;
QLineEdit* sourceDirectoryValue;
QLabel* targetDirectoryLabel;
QLineEdit* targetDirectoryValue;
QPushButton* optionsPushButton;
QPushButton* resizePhotosButton;
QMenuBar* menubar;
QStatusBar* statusbar;
};
#endif // MAINWINDOW_H_
mainwindow.cpp
#include "createNamedQTWidget.h"
#include "mainwindow.h"
#include <QMessageBox>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
setUpMainWindowUI();
connect(optionsPushButton, &QPushButton::clicked, this, &MainWindow::on_optionsPushButton_Clicked);
connect(resizePhotosButton, &QPushButton::clicked, this, &MainWindow::on_resizePhotosButton_Clicked);
}
MainWindow::~MainWindow()
{
}
/*
* Slots
*/
void MainWindow::on_resizedPhotos_valueChanged(std::size_t resizedPhotoCount)
{
resizedPhotosLcdNumber->display(static_cast<int>(resizedPhotoCount));
resizeProgressBar->setValue(static_cast<int>(resizedPhotoCount));
}
void MainWindow::on_photosToResizeCount_ValueChanged(std::size_t photosToResize)
{
filesToResizeLcdNumber->display(static_cast<int>(photosToResize));
resizeProgressBar->setRange(0, static_cast<int>(photosToResize));
}
void MainWindow::on_SourceDirectory_Changed(QString srcDir)
{
sourceDirectoryValue->setText(srcDir);
}
void MainWindow::on_TargetDirectory_Changed(QString targetDir)
{
targetDirectoryValue->setText(targetDir);
}
void MainWindow::on_optionsPushButton_Clicked()
{
emit mainWindowOptionsButtonPressed(true);
}
void MainWindow::on_resizePhotosButton_Clicked()
{
emit resizeAllPhotos();
}
/*
* Private UI methods
*/
void MainWindow::setUpMainWindowUI()
{
centralwidget = new QWidget(this);
centralwidget->setObjectName(QString::fromUtf8("centralwidget"));
mwLayout = new QVBoxLayout(centralwidget);
optionsPushButton = CreateNamedButton("Options", "optionsPushButton");
mwLayout->addWidget(optionsPushButton, 0, Qt::AlignHCenter);
setUpProgressDisplays();
setUpDirectoryDisplays();
resizePhotosButton = CreateNamedButton("Resize Photos", "resizePhotosButton");
resizePhotosButton->setDisabled(true);
mwLayout->addWidget(resizePhotosButton, 0, Qt::AlignHCenter);
resize(mainWindowWidth, mainWindowHeight);
setCentralWidget(centralwidget);
}
void MainWindow::setUpDirectoryDisplays()
{
sourceDirectoryLabel = createNamedLabel("Source Directory", "sourceDirectoryLabel");
mwLayout->addWidget(sourceDirectoryLabel, 0, Qt::AlignHCenter);
sourceDirectoryValue = createDirectoryDisplayLab("sourceDirectoryValue");
mwLayout->addWidget(sourceDirectoryValue, 0, Qt::AlignHCenter);
targetDirectoryLabel = createNamedLabel("Target Directory", "targetDirectoryLabel");
mwLayout->addWidget(targetDirectoryLabel, 0, Qt::AlignHCenter);
targetDirectoryValue = createDirectoryDisplayLab("targetDirectoryValue");
mwLayout->addWidget(targetDirectoryValue, 0, Qt::AlignHCenter);
}
void MainWindow::setUpProgressDisplays()
{
photoCountHBoxLayout = new QHBoxLayout;
mwLayout->addLayout(photoCountHBoxLayout, 0);
filesToResizeVBoxLayout = new QVBoxLayout;
photoCountHBoxLayout->addLayout(filesToResizeVBoxLayout, 0);
resizePhotosVBoxLayout = new QVBoxLayout;
photoCountHBoxLayout->addLayout(resizePhotosVBoxLayout, 0);
filesToResizeLabel = createNamedLabel("Photos to Resize", "filesToResizeLabel");
filesToResizeVBoxLayout->addWidget(filesToResizeLabel, 0, Qt::AlignHCenter);
filesToResizeLcdNumber = createAndConfigureLCD("filesToResizeLcdNumber");
filesToResizeVBoxLayout->addWidget(filesToResizeLcdNumber, 0, Qt::AlignHCenter);
resizedPhotosLabel = createNamedLabel("Resized Photos","resizedPhotosLabel");
resizePhotosVBoxLayout->addWidget(resizedPhotosLabel, 0, Qt::AlignHCenter);
resizedPhotosLcdNumber = createAndConfigureLCD("resizedPhotosLcdNumber");
resizePhotosVBoxLayout->addWidget(resizedPhotosLcdNumber, 0, Qt::AlignHCenter);
resizeProgressBar = createAndConfigureProgressBar("resizeProgressBar");
mwLayout->addWidget(resizeProgressBar, 0, Qt::AlignHCenter);
}
QLCDNumber *MainWindow::createAndConfigureLCD(const char *lcdName, const int initValue)
{
QLCDNumber* lcd = createNamedQTWidget<QLCDNumber>(lcdName);
QString lcdStyle = generateWidthAndHeightStyleString(lcdWidth, lcdHeight);
lcdStyle += " background-color: black; color: yellow;";
lcd->setStyleSheet(lcdStyle);
lcd->setSegmentStyle(QLCDNumber::Flat);
lcd->setDigitCount(lcdDigitCount);
lcd->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
lcd->display(0);
return lcd;
}
QProgressBar *MainWindow::createAndConfigureProgressBar(const char* objectName, const int initValue)
{
QProgressBar* progressBar = createNamedQTWidget<QProgressBar>(objectName, centralwidget);
progressBar->setRange(0, 200);
progressBar->setValue(initValue);
progressBar->setStyleSheet(generateWidthAndHeightStyleString(
static_cast<int>(maxOjectWidth * 0.7), progressBarHeight));
progressBar->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
return progressBar;
}
int MainWindow::getLabelWidth(QLabel *lab)
{
QFont currentFont = lab->font();
QFontMetrics fm(currentFont);
return fm.horizontalAdvance(lab->text());
}
QLabel *MainWindow::createNamedLabel(const char *labText, const char *labName)
{
QLabel* newLabel = createNameQTWidgetWithText<QLabel>(labText, labName, centralwidget);
newLabel->setStyleSheet(generateWidthAndHeightStyleString(getLabelWidth(newLabel), labelHeight));
newLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
return newLabel;
}
QLineEdit* MainWindow::createDirectoryDisplayLab(const char *labName)
{
QLineEdit* newDisplay = createNamedQTWidget<QLineEdit>(labName, centralwidget);
newDisplay->setReadOnly(true);
QString displayStyle = generateWidthAndHeightStyleString(maxOjectWidth, labelHeight);
newDisplay->setStyleSheet(displayStyle);
newDisplay->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
return newDisplay;
}
QPushButton* MainWindow::CreateNamedButton(const char* buttonText, const char* buttonName)
{
QPushButton* newButton = createNameQTWidgetWithText<QPushButton>(buttonText, buttonName, centralwidget);
newButton->setStyleSheet(generateWidthAndHeightStyleString(newButton->width(), buttonHeight));
return newButton;
}
QString MainWindow::generateWidthAndHeightStyleString(const int width, const int height)
{
QString widthAndHeightStyleString("width: ");
widthAndHeightStyleString += QString::number(width) + "; height:";
widthAndHeightStyleString += QString::number(height) += ";";
return widthAndHeightStyleString;
}
createNamedQTWidget.h
#ifndef CREATENAMEDQTWIDGET_H_
#define CREATENAMEDQTWIDGET_H_
/*
* Providing object names makes using QMetaObject::connectSlotsByName() easier.
* Properly named slots can be easily connected to their signals.
*/
class QWidget;
/*
* General Widget types.
*/
template <typename WidgetType>
WidgetType *createNamedQTWidget(const char* objectName, QWidget* parent=nullptr)
{
WidgetType* objectPointer = new WidgetType(parent);
objectPointer->setObjectName(objectName);
return objectPointer;
}
/*
* For buttons and checkboxes.
*/
template <typename WidgetType>
WidgetType *createNameQTWidgetWithText(const char* textContent, const char* objectName, QWidget* parent=nullptr)
{
WidgetType* objectPointer = new WidgetType(textContent, parent);
objectPointer->setObjectName(objectName);
return objectPointer;
}
#endif // CREATENAMEDQTWIDGET_H_
DirectoryLineEdit.h
#ifndef DIRECTORYLINEEDIT_H_
#define DIRECTORYLINEEDIT_H_
#include <QLineEdit>
#include <QFileDialog>
#include <QFocusEvent>
class DirectoryLineEdit : public QLineEdit {
Q_OBJECT
public:
explicit DirectoryLineEdit(const char* dleName, const char* title, int leWidth, QWidget *parent = nullptr)
: QLineEdit{parent}, fileDialogTitle{title}
{
setObjectName(QString::fromUtf8(dleName));
setStyleSheet("width: " + QString::number(leWidth) + "px;");
setReadOnly(true);
}
// The original version of this function caused an endless loop when loosing
// focus. This function has had help
// https://stackoverflow.com/questions/79428499/infinite-focus-loop-in-qt
void focusInEvent(QFocusEvent *event)
{
if (event->reason() == Qt::MouseFocusReason ||
event->reason() == Qt::TabFocusReason ||
event->reason() == Qt::BacktabFocusReason ||
event->reason() == Qt::ShortcutFocusReason)
{
QString textToChange = text();
textToChange = QFileDialog::getExistingDirectory(nullptr, fileDialogTitle,
textToChange, QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
if (!textToChange.isEmpty())
{
setText(textToChange);
}
}
QWidget::focusInEvent(event);
}
private:
QString fileDialogTitle;
};
#endif // DIRECTORYLINEEDIT_H_
NumericLineEdit.h
#ifndef NUMERICLINEDIT_H_
#define NUMERICLINEDIT_H_
#include <QLineEdit>
#include <QFocusEvent>
class NumericLineEdit : public QLineEdit {
Q_OBJECT
public:
explicit NumericLineEdit(const char* nleName, QWidget *parent = nullptr)
: QLineEdit{parent}
{
setObjectName(QString::fromUtf8(nleName));
setStyleSheet(numericLEStyle);
setMaxLength(maxDigitsNumericLE);
}
/*
* Due to how message boxes change the focus when reporting error conditions,
* how the line edit gains and loses focus is important. Multiple signals
* for editing were being reported when the user didn't edit.
*/
void focusInEvent(QFocusEvent *event)
{
if (event->reason() == Qt::MouseFocusReason ||
event->reason() == Qt::TabFocusReason ||
event->reason() == Qt::BacktabFocusReason ||
event->reason() == Qt::ShortcutFocusReason)
{
sendMySiginal = true;
}
QWidget::focusInEvent(event);
}
void focusOutEvent(QFocusEvent *event)
{
if (event->reason() == Qt::MouseFocusReason ||
event->reason() == Qt::TabFocusReason ||
event->reason() == Qt::BacktabFocusReason ||
event->reason() == Qt::ShortcutFocusReason)
{
if (isModified())
{
emit userEditComplete(text());
}
}
sendMySiginal = false;
QWidget::focusInEvent(event);
}
void highlightError(bool isError)
{
setStyleSheet(isError? numericLEStyleError : numericLEStyle);
}
signals:
void userEditComplete(QString);
private:
bool sendMySiginal = false;
const int maxDigitsNumericLE = 5;
const char* numericLEStyle = "width: 60px; background-color: white;";
const char* numericLEStyleError = "width: 60px; background-color: yellow;";
};
#endif // NUMERICLINEDIT_H_
OptionErrorCode.h
#ifndef OPTIONERRORCODE_H_
#define OPTIONERRORCODE_H_
#include <QString>
using OptionErrorCode = unsigned int;
const OptionErrorCode maxWidthError = 0x0001;
const OptionErrorCode maxHeightError = 0x0002;
const OptionErrorCode scaleFactorError = 0x0004;
const OptionErrorCode maintainRatioError = 0x0008;
const OptionErrorCode missingSizeError = 0x0010;
const OptionErrorCode overwriteWarning = 0x0011;
struct OptionErrorSignalContents
{
OptionErrorCode errorCode;
QString errorMessage;
};
#endif // OPTIONERRORCODE_H_
OptionsInitStruct.h
#ifndef OPTIONSINITSTRUCT_H_
#define OPTIONSINITSTRUCT_H_
#include <string>
struct OptionsInitStruct
{
bool fixFileName = false;
bool processJPGFiles = true;
bool processPNGFiles = false;
bool overWriteFiles = false;
std::string sourceDirectory;
std::string targetDirectory;
std::string relocDirectory;
std::string resizedPostfix;
bool displayResized = false;
bool maintainRatio = false;
std::size_t maxWidth = 0;
std::size_t maxHeight = 0;
unsigned int scaleFactor = 0;
};
#endif // OPTIONSINITSTRUCT_H_
OptionsDialog.h
#ifndef OPTIONSDIALOG_H_
#define OPTIONSDIALOG_H_
#include "DirectoryLineEdit.h"
#include "NumericLineEdit.h"
#include "OptionErrorCode.h"
#include "OptionsInitStruct.h"
#include <QVariant>
#include <QAbstractButton>
#include <QApplication>
#include <QCheckBox>
#include <QDialog>
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QGroupBox>
#include <QLabel>
#include <QLineEdit>
#include <QString>
#include <QVBoxLayout>
class OptionsDialog : public QDialog
{
Q_OBJECT
public:
explicit OptionsDialog(QWidget* parent = nullptr);
~OptionsDialog();
public slots:
void initOptionsValues(OptionsInitStruct modelValues);
void onModelErrorSignal(OptionErrorSignalContents eMessage) { handelModelError(eMessage); };
void onModelClearError(OptionErrorCode clearedError) { clearModelError(clearedError); };
void highlightOverwriteCB(bool highlight)
{
overwriteCheckBox->setStyleSheet(
highlight? "background-color: yellow;" : "background-color: none;");
}
signals:
void sourceDirectoryLEChanged(QString newSrcDir);
void targetDirectoryLEChanged(QString newTargetDir);
void optionsDoneFindPhotoFiles(bool ready);
void optionsJPGFileTypeCheckBoxChanged(bool checked);
void optionsPNGFileTypecheckBoxChanged(bool checked);
void optionsSafeWebNameCheckBoxChanged(bool checked);
void optionsOverwriteCheckBoxChanged(bool checked);
void optionsaddExtensionLEChanged(QString newExtension);
void optionsMaintainRatioCBChanged(bool checked);
void optionsDisplayResizedCBChanged(bool checked);
void optionsMaxWidthLEChanged(QString maxWidthQS);
void optionsMaxHeightLEChanged(QString maxHeightQS);
void optionsScaleFactorLEChanged(QString scaleFactorQS);
void validateOptionsDialog();
private slots:
void onAccept();
void on_targetDirectoryLineEdit_textChanged() { emit targetDirectoryLEChanged(targetDirectoryLineEdit->text()); };
void on_sourceDirectoryLineEdit_textChanged() { emit sourceDirectoryLEChanged(sourceDirectoryLineEdit->text()); };
void on_addExtensionLineEdit_editingFinished() { emit optionsaddExtensionLEChanged(addExtensionLineEdit->text()); };
void on_maxWidthLineEdit_userEditComplete(QString newValue) { emit optionsMaxWidthLEChanged(newValue); };
void on_maxHeightLineEdit_userEditComplete(QString newValue) { emit optionsMaxHeightLEChanged(newValue); };
void on_scaleFactorLineEdit_userEditComplete(QString newValue) { emit optionsScaleFactorLEChanged(newValue); };
private:
void setUpOtionsDialogUI();
QGroupBox* setUpFileGroupBox();
void connectFileGroupCheckBoxes();
void connectPhotoGroupCheckBoxes();
QGroupBox* setUpPhotoOptionGroupBox();
QDialogButtonBox* setUpOptionsButtonBox();
QFormLayout* createNamedFormLayoutWithPolicy(const char *formName);
void handelModelError(const OptionErrorSignalContents &eMessage);
void clearModelError(const OptionErrorCode clearedError);
void showErrorDisableOKButton(QString error);
void widgetHighlightError(const OptionErrorCode error, bool highlight);
void handleMaintainRatioError(bool isError);
void handleMissingSizeError(bool isError);
void connectDialogButtons();
QGroupBox* fileAndDirectoryGroupBox;
QCheckBox* JPGFileTypeCheckBox;
QCheckBox* PNGFileTypecheckBox;
QCheckBox* fixFileNameCheckBox;
QCheckBox* overwriteCheckBox;
DirectoryLineEdit* sourceDirectoryLineEdit;
DirectoryLineEdit* targetDirectoryLineEdit;
QLineEdit* addExtensionLineEdit;
QGroupBox* photoOptionsBox;
QCheckBox* maintainRatioCheckBox;
QCheckBox* displayResizedCheckBox;
NumericLineEdit* maxWidthLineEdit;
NumericLineEdit* maxHeightLineEdit;
NumericLineEdit* scaleFactorLineEdit;
QDialogButtonBox* optionsButtonBox;
QFormLayout* photoOptionsLayout;
QFormLayout* fileAndDirectorylayout;
QVBoxLayout* optionsDialogLayout;
const int groupBoxSpacing = 60;
const int directorLEWidth = 400;
OptionErrorCode modelHasErrors = 0;
};
#endif // OPTIONSDIALOG_H_
OptionsDialog.cpp
#include "createNamedQTWidget.h"
#include "DirectoryLineEdit.h"
#include "NumericLineEdit.h"
#include "OptionsDialog.h"
#include <QVariant>
#include <QAbstractButton>
#include <QApplication>
#include <QCheckBox>
#include <QCloseEvent>
#include <QDialog>
#include <QDialogButtonBox>
#include <QFileDialog>
#include <QFormLayout>
#include <QGroupBox>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QPushButton>
#include <QString>
OptionsDialog::OptionsDialog(QWidget *parent) :
QDialog(parent)
{
setObjectName(QString::fromUtf8("Options"));
setUpOtionsDialogUI();
/*
* QMetaObject::connectSlotsByName() does not connect Dialog Button Box buttons
* or checkboxes. This makes it necessary to connect the buttons and checkboxes
* explicitly.
*/
connectDialogButtons();
connectFileGroupCheckBoxes();
connectPhotoGroupCheckBoxes();
QMetaObject::connectSlotsByName(this);
}
OptionsDialog::~OptionsDialog()
{
}
void OptionsDialog::setUpOtionsDialogUI()
{
optionsDialogLayout = new QVBoxLayout;
optionsDialogLayout->setObjectName("optionsDialogLayout");
optionsDialogLayout->addWidget(setUpFileGroupBox(), 0, Qt::AlignHCenter);
optionsDialogLayout->addWidget(setUpPhotoOptionGroupBox(), 0, Qt::AlignHCenter);
optionsButtonBox = setUpOptionsButtonBox();
optionsDialogLayout->addWidget(optionsButtonBox, 0, Qt::AlignHCenter);
optionsDialogLayout->setSpacing(groupBoxSpacing);
setLayout(optionsDialogLayout);
QString dialogTitle = windowTitle();
dialogTitle += " Options:";
setWindowTitle(dialogTitle);
}
QGroupBox* OptionsDialog::setUpPhotoOptionGroupBox()
{
photoOptionsBox = new QGroupBox("Photo Options", this);
QFormLayout* photoOptionsLayout =
createNamedFormLayoutWithPolicy("photoOptionsLayout");
maintainRatioCheckBox = createNameQTWidgetWithText<QCheckBox>(
"Maintain Ratio", "maintainRatioCheckBox", this);
photoOptionsLayout->addRow(maintainRatioCheckBox);
displayResizedCheckBox = createNameQTWidgetWithText<QCheckBox>(
"Display Resized Photo", "displayResizedCheckBox", this);
photoOptionsLayout->addRow(displayResizedCheckBox);
maxWidthLineEdit = new NumericLineEdit("maxWidthLineEdit");
photoOptionsLayout->addRow("Maximum Photo Width", maxWidthLineEdit);
maxHeightLineEdit = new NumericLineEdit("maxHeightLineEdit");
photoOptionsLayout->addRow("Maximum Photo Height", maxHeightLineEdit);
scaleFactorLineEdit = new NumericLineEdit("scaleFactorLineEdit");
photoOptionsLayout->addRow("Scale Factor", scaleFactorLineEdit);
photoOptionsBox->setLayout(photoOptionsLayout);
return photoOptionsBox;
}
QDialogButtonBox *OptionsDialog::setUpOptionsButtonBox()
{
QDialogButtonBox *buttonBox = new QDialogButtonBox(this);
buttonBox->setObjectName(QString::fromUtf8("optionsButtonBox"));
buttonBox->setGeometry(QRect(0, 500, 341, 32));
buttonBox->setOrientation(Qt::Horizontal);
buttonBox->setStandardButtons(QDialogButtonBox::Cancel|QDialogButtonBox::Ok);
return buttonBox;
}
QGroupBox* OptionsDialog::setUpFileGroupBox()
{
QFormLayout* fileAndDirectorylayout = createNamedFormLayoutWithPolicy(
"fileAndDirectorylayout");
JPGFileTypeCheckBox = createNameQTWidgetWithText<QCheckBox>(
"JPG files", "JPGFileTypeCheckBox", this);
fileAndDirectorylayout->addRow(JPGFileTypeCheckBox);
PNGFileTypecheckBox = createNameQTWidgetWithText<QCheckBox>(
"PNG Files", "PNGFileTypecheckBox", this);
fileAndDirectorylayout->addRow(PNGFileTypecheckBox);
fixFileNameCheckBox = createNameQTWidgetWithText<QCheckBox>(
"Safe Web Name", "fixFileNameCheckBox", this);
fileAndDirectorylayout->addRow(fixFileNameCheckBox);
overwriteCheckBox = createNameQTWidgetWithText<QCheckBox>(
"Overwrite Existing Files", "overwriteCheckBox", this);
fileAndDirectorylayout->addRow(overwriteCheckBox);
sourceDirectoryLineEdit = new DirectoryLineEdit("sourceDirectoryLineEdit", "Source Directory", directorLEWidth, this);
fileAndDirectorylayout->addRow("Source Directory", sourceDirectoryLineEdit);
targetDirectoryLineEdit = new DirectoryLineEdit("targetDirectoryLineEdit", "Target Directory", directorLEWidth, this);
fileAndDirectorylayout->addRow("Target Directory", targetDirectoryLineEdit);
addExtensionLineEdit = createNamedQTWidget<QLineEdit>("addExtensionLineEdit", this);
addExtensionLineEdit->setStyleSheet("width: 200px;");
addExtensionLineEdit->setMaxLength(20);
fileAndDirectorylayout->addRow("Add Extension", addExtensionLineEdit);
fileAndDirectoryGroupBox = new QGroupBox("File Type and Directory Options", this);
fileAndDirectoryGroupBox->setLayout(fileAndDirectorylayout);
return fileAndDirectoryGroupBox;
}
void OptionsDialog::connectDialogButtons()
{
QObject::connect(optionsButtonBox, &QDialogButtonBox::accepted,
this, &OptionsDialog::onAccept);
QObject::connect(optionsButtonBox, &QDialogButtonBox::rejected,
this, qOverload<>(&QDialog::reject));
}
/*
* Please pardon the repetition of code in the following 2 functions. I am still
* researching how to pass a signal that needs to be emitted into a function.
*/
void OptionsDialog::connectPhotoGroupCheckBoxes()
{
connect(maintainRatioCheckBox, &QCheckBox::toggled, [this]()
{
emit optionsMaintainRatioCBChanged(maintainRatioCheckBox->isChecked());
}
);
connect(displayResizedCheckBox, &QCheckBox::toggled, [this]()
{
emit optionsDisplayResizedCBChanged(displayResizedCheckBox->isChecked());
}
);
}
void OptionsDialog::connectFileGroupCheckBoxes()
{
connect(JPGFileTypeCheckBox, &QCheckBox::toggled, [this]()
{
emit optionsJPGFileTypeCheckBoxChanged(JPGFileTypeCheckBox->isChecked());
}
);
connect(PNGFileTypecheckBox, &QCheckBox::toggled, [this]()
{
emit optionsPNGFileTypecheckBoxChanged(PNGFileTypecheckBox->isChecked());
}
);
connect(fixFileNameCheckBox, &QCheckBox::toggled, [this]()
{
emit optionsSafeWebNameCheckBoxChanged(fixFileNameCheckBox->isChecked());
}
);
connect(overwriteCheckBox, &QCheckBox::toggled, [this]()
{
emit optionsOverwriteCheckBoxChanged(overwriteCheckBox->isChecked());
}
);
}
QFormLayout* OptionsDialog::createNamedFormLayoutWithPolicy(const char *formName)
{
QFormLayout* newFormLayout = createNamedQTWidget<QFormLayout>(formName);
newFormLayout->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
return newFormLayout;
}
void OptionsDialog::handelModelError(const OptionErrorSignalContents &eMessage)
{
OptionErrorCode eCode = eMessage.errorCode;
modelHasErrors |= eCode;
widgetHighlightError(eCode, true);
showErrorDisableOKButton(eMessage.errorMessage);
}
void OptionsDialog::clearModelError(const OptionErrorCode clearedError)
{
modelHasErrors &= ~clearedError;
widgetHighlightError(clearedError, false);
if (!modelHasErrors)
{
optionsButtonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
}
}
void OptionsDialog::showErrorDisableOKButton(QString error)
{
QMessageBox errorMessageBox;
errorMessageBox.critical(0,"Error:", error);
errorMessageBox.setFixedSize(500,200);
optionsButtonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
}
void OptionsDialog::widgetHighlightError(const OptionErrorCode error, bool highlight)
{
NumericLineEdit* widgetToChange = nullptr;
switch (error)
{
default:
showErrorDisableOKButton("Unknown error code!");
return;
case maintainRatioError:
handleMaintainRatioError(highlight);
return;
case missingSizeError:
handleMissingSizeError(highlight);
return;
case maxWidthError:
widgetToChange = maxWidthLineEdit;
break;
case maxHeightError:
widgetToChange = maxHeightLineEdit;
break;
case scaleFactorError:
widgetToChange = scaleFactorLineEdit;
break;
}
if (widgetToChange)
{
widgetToChange->highlightError(highlight);
}
}
/*
* Multiple widgets need to be changed.
* The maintain ratio error indicates that both width and height have
* been specified which can cause ratio errors.
*/
void OptionsDialog::handleMaintainRatioError(bool isError)
{
maintainRatioCheckBox->setStyleSheet(isError? "background-color: yellow;" :
"background-color: none;");
maxWidthLineEdit->highlightError(isError);
maxHeightLineEdit->highlightError(isError);
}
/*
* Multiple widgets need to be changed.
*/
void OptionsDialog::handleMissingSizeError(bool isError)
{
maxWidthLineEdit->highlightError(isError);
maxHeightLineEdit->highlightError(isError);
scaleFactorLineEdit->highlightError(isError);
}
/*
* Slots for the widgets.
*/
void OptionsDialog::onAccept()
{
if (modelHasErrors)
{
QMessageBox::warning(this, "Errors", "Please correct the errors highlighted in yellow before closing.");
}
else
{
emit validateOptionsDialog();
}
}
void OptionsDialog::initOptionsValues(OptionsInitStruct modelValues)
{
sourceDirectoryLineEdit->setText(QString::fromStdString(modelValues.sourceDirectory));
targetDirectoryLineEdit->setText(QString::fromStdString(modelValues.targetDirectory));
JPGFileTypeCheckBox->setChecked(modelValues.processJPGFiles);
PNGFileTypecheckBox->setChecked(modelValues.processPNGFiles);
fixFileNameCheckBox->setChecked(modelValues.fixFileName);
overwriteCheckBox->setChecked(modelValues.overWriteFiles);
addExtensionLineEdit->setText(QString::fromStdString(modelValues.resizedPostfix));
maintainRatioCheckBox->setChecked(modelValues.maintainRatio);
displayResizedCheckBox->setChecked(modelValues.displayResized);
maxWidthLineEdit->setText(((modelValues.maxWidth > 0)?
QString::number(modelValues.maxWidth) : QString("")));
maxHeightLineEdit->setText(((modelValues.maxWidth > 0)?
QString::number(modelValues.maxHeight) : QString("")));
scaleFactorLineEdit->setText(((modelValues.maxWidth > 0)?
QString::number(modelValues.scaleFactor) : QString("")));
}
PhotoReducerModel.h
#ifndef PHOTOREDUCERMODEL_H_
#define PHOTOREDUCERMODEL_H_
#include <filesystem>
#include <opencv2/opencv.hpp>
#include "OptionErrorCode.h"
#include "OptionsInitStruct.h"
#include <QObject>
#include <QString>
#include <string>
#include <vector>
// std::filesystem can make lines very long.
namespace fs = std::filesystem;
struct PhotoFile
{
std::string inputName;
std::string outputName;
};
using PhotoFileList = std::vector<PhotoFile>;
using InputPhotoList = std::vector<fs::path>;
class PhotoReducerModel : public QObject
{
Q_OBJECT
public:
explicit PhotoReducerModel(const char * modelName, QObject *parent = nullptr);
~PhotoReducerModel() = default;
public slots:
void initializeMainWindow();
void initializeOptionsDialog();
void resizeAllPhotos();
void validateOptionsDialog();
/*
* File options slots.
*/
void optionsSourceDirectoryEdited(QString newSrcDir);
void optionsTargetDirectoryEdited(QString newTargetDir);
void optionsJPGCheckBoxChanged(bool checked) { processJPGFiles = checked; };
void optionsPNGCheckBoxChanged(bool checked) { processPNGFiles = checked; };
void optionsSafeWebNameChanged(bool checked) { fixFileName = checked; };
void optionsOverWriteFilesChanged(bool checked)
{
overWriteFiles = checked;
emit highlightOverWriteCB(false);
};
void optionsAddExtensionChanged(QString extension);
/*
* Photo options slots.
*/
void optionsMaxWidthChanged(QString maxWidthQS);
void optionsMaxHeightChanged(QString maxHeightQS);
void optionsScaleFactorChanged(QString scaleFactorQS);
void optionsMaintainRatioChanged(bool checked) { maintainRatio = checked; };
void optionsDisplayResizedChanged(bool checked) { displayResized = checked; };
signals:
void initOptionsValues(OptionsInitStruct modelValues);
void resizedPhotosCountValueChanged(std::size_t newValue);
void photosToResizeCountValueChanged(std::size_t newValue);
void sourceDirectoryValueChanged(QString newSrcDir);
void targetDirectoryValueChanged(QString newTargetDir);
void initMainWindowSourceDirectory(QString newSrcDir);
void initMainWindowTargetDirectory(QString newTargetDir);
void modelErrorSignal(OptionErrorSignalContents &errorSignal);
void modelClearError(OptionErrorCode clearedError);
void enableMainWindowResizePhotosButton();
void highlightOverWriteCB(bool highlight);
void acceptOptionsDialog();
private slots:
private:
// UI releated functions
void setResizedPhotoCount(std::size_t newValue);
void incrementResizedPhotoCount();
void setPhotosToResizeCount(std::size_t newValue);
void setSourceDirectory(QString newSrcDir);
void setTargetDirectory(QString newTargetDir);
// Processing and error checking
int qstringToInt(QString possibleNumber);
bool hasPhotoSize() { return maxWidth || maxHeight || scaleFactor; };
bool hasErrors() { return errorMask != 0; };
bool hasThisError(OptionErrorCode eCode) { return (errorMask & eCode) != 0; };
void setErrorSendErrorSignal(OptionErrorCode code, QString eMessage);
void clearError(OptionErrorCode code);
bool checkMaintainRatioErrors();
bool clearMaintainRatioErrorIfSet();
void clearMissingSizeErrorIfSet(std::size_t newSize);
void photoSizeValueError(OptionErrorCode code, QString dimension);
std::size_t processPhotoDimension(QString value, QString dimension, OptionErrorCode code);
void reportAnyAttemptedOverwrites();
// Get all the photo files in the source directory the user specified.
// Apply any name changes to the outout files.
void buildPhotoInputAndOutputList();
InputPhotoList findAllPhotos(fs::path& originsDir);
void addFilesToListByExtension(std::filesystem::path& cwd, const std::string& extLC,
const std::string& extUC, InputPhotoList& photoList);
std::string makeFileNameWebSafe(const std::string& inName);
std::string makeOutputFileName(const fs::path& inputFile, const fs::path& targetDir);
PhotoFileList copyInFileNamesToPhotoListAddOutFileNames(
InputPhotoList& inFileList, fs::path& targetDir);
// Process all the photo files found.
void resizeAllPhotosInList();
bool resizeAndSavePhoto(const PhotoFile& photoFile);
cv::Mat resizeByUserSpecification(cv::Mat& photo);
bool saveResizedPhoto(cv::Mat& resizedPhoto, const std::string webSafeName);
cv::Mat resizePhotoByPercentage(cv::Mat& photo, const unsigned int percentage);
cv::Mat resizePhotoByHeightMaintainGeometry(cv::Mat& photo);
cv::Mat resizePhotoByWidthMaintainGeometry(cv::Mat& photo);
cv::Mat resizePhoto(cv::Mat& photo, const std::size_t newWdith, const std::size_t newHeight);
/*
* Private Variables.
*/
std::size_t resizedPhotosCount = 0;
std::size_t photosToResizeCount = 0;
PhotoFileList photoFileList;
OptionErrorCode errorMask = 0;
/*
* File Options.
*/
bool fixFileName = false;
bool processJPGFiles = true;
bool processPNGFiles = false;
bool overWriteFiles = false;
std::string sourceDirectory;
std::string targetDirectory;
std::string relocDirectory;
std::string resizedPostfix;
/*
* Photo Options.
*/
bool displayResized = false;
bool maintainRatio = false;
std::size_t maxWidth = 0;
std::size_t maxHeight = 0;
unsigned int scaleFactor = 0;
std::size_t attemptedOverwrites = 0;
const unsigned int minScaleFactor = 20;
const unsigned int maxScaleFactor = 90;
};
#endif // PHOTOREDUCERMODEL_H_
PhotoReducerModel.cpp
#include <algorithm>
#include <cctype>
#include <filesystem>
#include <iterator>
#include <opencv2/opencv.hpp>
#include "PhotoReducerModel.h"
#include <QDir>
#include <QMessageBox>
#include <QString>
#include <ranges>
#include <string>
#include <vector>
PhotoReducerModel::PhotoReducerModel(const char* modelName, QObject *parent)
: QObject{parent}, displayResized{false}, maintainRatio{true}
{
setObjectName(QString::fromUtf8(modelName));
QString startDir = QDir::homePath();
setSourceDirectory(startDir);
setTargetDirectory(startDir);
setResizedPhotoCount(0);
setPhotosToResizeCount(0);
}
void PhotoReducerModel::setResizedPhotoCount(std::size_t newValue)
{
if (newValue != resizedPhotosCount) {
resizedPhotosCount = newValue;
emit resizedPhotosCountValueChanged(resizedPhotosCount);
}
}
void PhotoReducerModel::incrementResizedPhotoCount()
{
++resizedPhotosCount;
emit resizedPhotosCountValueChanged(resizedPhotosCount);
}
void PhotoReducerModel::optionsAddExtensionChanged(QString extension)
{
resizedPostfix = extension.toStdString();
}
void PhotoReducerModel::setPhotosToResizeCount(std::size_t newValue)
{
if (newValue != photosToResizeCount) {
photosToResizeCount = newValue;
emit photosToResizeCountValueChanged(photosToResizeCount);
}
}
void PhotoReducerModel::setSourceDirectory(QString newSrcDir)
{
std::string newDir = newSrcDir.toStdString();
if (newDir != sourceDirectory)
{
sourceDirectory = newDir;
emit sourceDirectoryValueChanged(newSrcDir);
}
}
void PhotoReducerModel::setTargetDirectory(QString newTargetDir)
{
std::string newDir = newTargetDir.toStdString();
if (newDir != targetDirectory)
{
targetDirectory = newDir;
emit targetDirectoryValueChanged(newTargetDir);
}
}
int PhotoReducerModel::qstringToInt(QString possibleNumber)
{
bool ok;
int output = static_cast<std::size_t>(possibleNumber.toInt(&ok, 10));
if (!ok)
{
output = -1;
}
return output;
}
void PhotoReducerModel::setErrorSendErrorSignal(OptionErrorCode code, QString eMessage)
{
errorMask |= code;
OptionErrorSignalContents eSignalContents;
eSignalContents.errorCode = code;
eSignalContents.errorMessage = eMessage;
emit modelErrorSignal(eSignalContents);
}
void PhotoReducerModel::clearError(OptionErrorCode code)
{
if (hasThisError(code))
{
errorMask &= ~code;
emit modelClearError(code);
}
}
bool PhotoReducerModel::checkMaintainRatioErrors()
{
if (maintainRatio && maxWidth && maxHeight)
{
setErrorSendErrorSignal(maintainRatioError, "To maintain the ratio of the picture, "
"only one of Max Width or Max Height may be specified!");
return true;
}
else
{
clearMaintainRatioErrorIfSet();
}
return false;
}
bool PhotoReducerModel::clearMaintainRatioErrorIfSet()
{
if (hasThisError(maintainRatioError))
{
clearError(maintainRatioError);
return true;
}
return false;
}
void PhotoReducerModel::clearMissingSizeErrorIfSet(std::size_t newSize)
{
if (newSize > 0 && hasThisError(missingSizeError))
{
clearError(missingSizeError);
}
}
void PhotoReducerModel::photoSizeValueError(OptionErrorCode code, QString dimension)
{
QString errorText = "Max " + dimension + " must be an integer value greater than zero!";
setErrorSendErrorSignal(code, errorText);
}
std::size_t PhotoReducerModel::processPhotoDimension(QString value, QString dimension, OptionErrorCode code)
{
int newValue = 0;
if (value.isEmpty())
{
// if value was previously set and this is correcting an error
clearMaintainRatioErrorIfSet();
}
else
{
newValue = qstringToInt(value);
if (newValue > 0)
{
if (!checkMaintainRatioErrors())
{
clearError(code);
}
}
else
{
// if value was not previously set and this is correcting an error
if (!(newValue == 0 && clearMaintainRatioErrorIfSet()))
{
photoSizeValueError(code, dimension);
newValue = 0;
}
}
}
return static_cast<std::size_t>(newValue);
}
void PhotoReducerModel::reportAnyAttemptedOverwrites()
{
if (attemptedOverwrites)
{
QString overwriteMsg = QString::number(attemptedOverwrites);
overwriteMsg += " photos will not be resized because existing files "
"would be overwritten. To overwrite these files open the options"
" dialog and click the Overwrite checkbox";
QMessageBox warningMessageBox;
warningMessageBox.warning(0,"Warning:", overwriteMsg);
warningMessageBox.setFixedSize(500,200);
}
}
/*
* Slots
*/
void PhotoReducerModel::initializeMainWindow()
{
emit initMainWindowSourceDirectory(QString::fromStdString(sourceDirectory));
emit initMainWindowTargetDirectory(QString::fromStdString(targetDirectory));
}
void PhotoReducerModel::initializeOptionsDialog()
{
OptionsInitStruct modelValues;
modelValues.sourceDirectory = sourceDirectory;
modelValues.targetDirectory = targetDirectory;
modelValues.processJPGFiles = processJPGFiles;
modelValues.processPNGFiles = processPNGFiles;
modelValues.fixFileName = fixFileName;
modelValues.overWriteFiles = overWriteFiles;
modelValues.resizedPostfix = resizedPostfix;
modelValues.maintainRatio = maintainRatio;
modelValues.displayResized = displayResized;
modelValues.maxWidth = maxWidth;
modelValues.maxHeight = maxHeight;
modelValues.scaleFactor = scaleFactor;
emit initOptionsValues(modelValues);
if (!overWriteFiles && attemptedOverwrites)
{
emit highlightOverWriteCB(true);
}
}
void PhotoReducerModel::resizeAllPhotos()
{
resizeAllPhotosInList();
}
void PhotoReducerModel::validateOptionsDialog()
{
if (!hasPhotoSize())
{
setErrorSendErrorSignal(missingSizeError, "Please provide a new size for the photo!");
}
else if (!checkMaintainRatioErrors())
{
buildPhotoInputAndOutputList();
reportAnyAttemptedOverwrites();
emit acceptOptionsDialog();
}
}
void PhotoReducerModel::optionsSourceDirectoryEdited(QString newSrcDir)
{
setSourceDirectory(newSrcDir);
}
void PhotoReducerModel::optionsTargetDirectoryEdited(QString newTargetDir)
{
setTargetDirectory(newTargetDir);
}
void PhotoReducerModel::optionsMaxWidthChanged(QString maxWidthQS)
{
maxWidth = processPhotoDimension(maxWidthQS, "Width", maxWidthError);
checkMaintainRatioErrors();
clearMissingSizeErrorIfSet(maxWidth);
}
void PhotoReducerModel::optionsMaxHeightChanged(QString maxHeightQS)
{
maxHeight = processPhotoDimension(maxHeightQS, "Height", maxHeightError);
checkMaintainRatioErrors();
clearMissingSizeErrorIfSet(maxHeight);
}
void PhotoReducerModel::optionsScaleFactorChanged(QString scaleFactorQS)
{
if (!scaleFactorQS.isEmpty())
{
int testscaleFactor = qstringToInt(scaleFactorQS);
if (testscaleFactor >= minScaleFactor && testscaleFactor <= maxScaleFactor)
{
scaleFactor = testscaleFactor;
clearError(scaleFactorError);
clearMissingSizeErrorIfSet(scaleFactorError);
}
else
{
QString eText = "Scale Factor must be an integer value between " +
QString::number(minScaleFactor) + " and " +
QString::number(minScaleFactor);
setErrorSendErrorSignal(scaleFactorError, eText);
}
}
}
/*
* Model Business Logic Implementation
*/
/*
* The following code originated in the 2 previous versions of the Photo Reduction Tool
* in the file photofilefinder.[h,cpp]. It has been modified to be a part of the model,
* which decreases the number of parameters of some functions. Since the tool allows
* the user to chose the directories to use, some error checking is no longer needed.
*/
void PhotoReducerModel::buildPhotoInputAndOutputList()
{
fs::path sourceDir = sourceDirectory;
attemptedOverwrites = 0; // Clear any previous overwrite attempts
InputPhotoList inputPhotoList = findAllPhotos(sourceDir);
if (inputPhotoList.size())
{
fs::path targetDir = targetDirectory;
photoFileList = copyInFileNamesToPhotoListAddOutFileNames(inputPhotoList, targetDir);
setPhotosToResizeCount(photoFileList.size());
emit enableMainWindowResizePhotosButton();
}
}
InputPhotoList PhotoReducerModel::findAllPhotos(fs::path &originsDir)
{
InputPhotoList tempFileList;
if (processJPGFiles)
{
addFilesToListByExtension(originsDir, ".jpg", ".JPG", tempFileList);
}
if (processPNGFiles)
{
addFilesToListByExtension(originsDir, ".png", ".PNG", tempFileList);
}
return tempFileList;
}
void PhotoReducerModel::addFilesToListByExtension(
std::filesystem::path &cwd, const std::string &extLC,
const std::string &extUC, InputPhotoList &photoList
)
{
auto is_match = [extLC, extUC](auto f) {
return f.path().extension().string() == extLC ||
f.path().extension().string() == extUC;
};
auto files = fs::directory_iterator{ cwd }
| std::views::filter([](auto& f) { return f.is_regular_file(); })
| std::views::filter(is_match);
std::ranges::copy(files, std::back_inserter(photoList));
}
std::string PhotoReducerModel::makeFileNameWebSafe(const std::string &inName)
{
std::string webSafeName;
auto toUnderScore = [](unsigned char c) -> unsigned char { return std::isalnum(c)? c : '_'; };
std::ranges::transform(inName, std::back_inserter(webSafeName), toUnderScore);
return webSafeName;
}
std::string PhotoReducerModel::makeOutputFileName(const fs::path &inputFile, const fs::path &targetDir)
{
std::string ext = inputFile.extension().string();
std::string outputFileName = inputFile.stem().string();
if (fixFileName)
{
outputFileName = makeFileNameWebSafe(outputFileName);
}
if (!resizedPostfix.empty())
{
outputFileName += "." + resizedPostfix;
}
outputFileName += ext;
fs::path targetFile = targetDir;
targetFile.append(outputFileName);
if (fs::exists(targetFile) && !overWriteFiles)
{
++attemptedOverwrites;
targetFile.clear();
}
return targetFile.string();
}
PhotoFileList PhotoReducerModel::copyInFileNamesToPhotoListAddOutFileNames(
InputPhotoList &inFileList, fs::path &targetDir)
{
PhotoFileList photoFileList;
for (auto const& file: inFileList)
{
PhotoFile currentPhoto;
currentPhoto.inputName = file.string();
currentPhoto.outputName = makeOutputFileName(file, targetDir);
photoFileList.push_back(currentPhoto);
}
return photoFileList;
}
/*
* Resize all photos in the list of photo files.
*
* The following code originated in the 2 previous versions of the Photo Reduction Tool
* in the file PhotoResizer.[h,cpp]. It has been modified to be a part of the model,
* which decreases the number of parameters of some functions.
*/
void PhotoReducerModel::resizeAllPhotosInList()
{
for (auto photo: photoFileList)
{
if (resizeAndSavePhoto(photo))
{
incrementResizedPhotoCount();
}
}
}
bool PhotoReducerModel::resizeAndSavePhoto(const PhotoFile &photoFile)
{
// Possibly file already exists and user did not specify --overwrite
if (photoFile.outputName.empty())
{
return false;
}
cv::Mat photo = cv::imread(photoFile.inputName);
if (photo.empty()) {
QString eMsg = "Could not read photo ";
eMsg += QString::fromStdString(photoFile.inputName);
QMessageBox errorMessageBox;
errorMessageBox.critical(0,"Error:", eMsg);
errorMessageBox.setFixedSize(500,300);
return false;
}
cv::Mat resized = resizeByUserSpecification(photo);
if (displayResized)
{
cv::imshow("Resized Photo", resized);
cv::waitKey(0);
}
return saveResizedPhoto(resized, photoFile.outputName);
}
cv::Mat PhotoReducerModel::resizeByUserSpecification(cv::Mat &photo)
{
if (maxWidth > 0 && maxHeight > 0)
{
return resizePhoto(photo, maxWidth, maxHeight);
}
if (scaleFactor > 0)
{
return resizePhotoByPercentage(photo, scaleFactor);
}
if (maintainRatio)
{
if (maxWidth > 0)
{
return resizePhotoByWidthMaintainGeometry(photo);
}
if (maxHeight > 0)
{
return resizePhotoByHeightMaintainGeometry(photo);
}
return photo;
}
else
{
if (maxWidth > 0 && maxHeight == 0)
{
return resizePhotoByWidthMaintainGeometry(photo);
}
if (maxHeight > 0 && maxWidth == 0)
{
return resizePhotoByHeightMaintainGeometry(photo);
}
}
return photo;
}
bool PhotoReducerModel::saveResizedPhoto(cv::Mat &resizedPhoto, const std::string webSafeName)
{
bool saved = cv::imwrite(webSafeName, resizedPhoto);
if (!saved) {
QString eMsg = "Could not write photo ";
eMsg += QString::fromStdString(webSafeName);
QMessageBox errorMessageBox;
errorMessageBox.critical(0,"Error:", eMsg);
errorMessageBox.setFixedSize(500,300);
}
// Prevent memory leak.
resizedPhoto.release();
return saved;
}
cv::Mat PhotoReducerModel::resizePhotoByPercentage(cv::Mat &photo, const unsigned int percentage)
{
double percentMult = static_cast<double>(percentage)/100.0;
// Retain the current photo geometry.
std::size_t newWidth = static_cast<int>(photo.cols * percentMult);
std::size_t newHeight = static_cast<int>(photo.rows * percentMult);
return resizePhoto(photo, newWidth, newHeight);
}
cv::Mat PhotoReducerModel::resizePhotoByHeightMaintainGeometry(cv::Mat &photo)
{
if (static_cast<std::size_t>(photo.rows) <= maxHeight)
{
return photo;
}
double ratio = static_cast<double>(maxHeight) / static_cast<double>(photo.rows);
std::size_t newWidth = static_cast<int>(photo.cols * ratio);
return resizePhoto(photo, newWidth, maxHeight);
}
cv::Mat PhotoReducerModel::resizePhotoByWidthMaintainGeometry(cv::Mat &photo)
{
if (static_cast<std::size_t>(photo.cols) <= maxWidth)
{
return photo;
}
double ratio = static_cast<double>(maxWidth) / static_cast<double>(photo.cols);
std::size_t newHeight = static_cast<int>(photo.rows * ratio);
return resizePhoto(photo, maxWidth, newHeight);
}
cv::Mat PhotoReducerModel::resizePhoto(cv::Mat &photo, const std::size_t newWdith, const std::size_t newHeight)
{
cv::Size newSize(newWdith, newHeight);
cv::Mat resizedPhoto;
cv::resize(photo, resizedPhoto, newSize, 0, 0, cv::INTER_AREA);
// Prevent memory leak
photo.release();
return resizedPhoto;
}
SignalRouterController.h
#ifndef SIGNALROUTERCONTROLLER_H_
#define SIGNALROUTERCONTROLLER_H_
#include "mainwindow.h"
#include "PhotoReducerModel.h"
#include "OptionsDialog.h"
#include <QObject>
#include <QString>
/*
* The purpose of this class is to control the the Photo Reducer Tool. It
* creates the model and the different views of the model. Most of the signals
* and slots of the objects will be connected in this class.
*
* Signals to create new views are handled by this class.
*/
class SignalRouterController: public QObject
{
Q_OBJECT
public:
explicit SignalRouterController(const char * controllerName, QObject *parent = nullptr);
~SignalRouterController() = default;
void createModel();
void creatMainWindow();
void connectModelAndMainWindowSignalsToSlots();
void connectControllerAndModelSignalsToSlots();
void connectControllerAndMainWindowSignalsToSlots();
void initMainWindowValuesAndShow();
public slots:
void mainWindowOptionsButtonPressedCreateOptionsDialog(bool doINeedSignalContents);
void mainWindowResizePhotosButtonClicked() { emit resizeAllPhotos(); };
void enableMainWindowResizePhotosButton() { emit enablePhotoResizing(); };
void acceptOptionsDialog();
signals:
void mainWindowReadyForInitialization();
void optionDialogReadyForInitialization();
void enablePhotoResizing();
void resizeAllPhotos();
private slots:
private:
void createOptionsDialog();
void connectModelAndOptionsDialogSignalsToSlots();
void connectOptionDialogOutToModelIn();
void connectModelOutToOptionDialogIn();
PhotoReducerModel* model;
MainWindow* mainWindow;
OptionsDialog* optionsDialog;
};
#endif // SIGNALROUTERCONTROLLER_H_
SignalRouterController.cpp
#include "SignalRouterController.h"
#include <QString>
SignalRouterController::SignalRouterController(const char* objectName, QObject *parent)
: QObject{parent}
{
setObjectName(QString::fromUtf8(objectName));
}
void SignalRouterController::createModel()
{
model = new PhotoReducerModel("TheModel", this);
}
void SignalRouterController::creatMainWindow()
{
mainWindow = new MainWindow();
mainWindow->setObjectName("MainWindow");
}
void SignalRouterController::connectModelAndMainWindowSignalsToSlots()
{
connect(model, &PhotoReducerModel::initMainWindowSourceDirectory,
mainWindow, &MainWindow::on_SourceDirectory_Changed);
connect(model, &PhotoReducerModel::initMainWindowTargetDirectory,
mainWindow, &MainWindow::on_TargetDirectory_Changed);
connect(model, &PhotoReducerModel::resizedPhotosCountValueChanged,
mainWindow, &MainWindow::on_resizedPhotos_valueChanged);
connect(model, &PhotoReducerModel::photosToResizeCountValueChanged,
mainWindow, &MainWindow::on_photosToResizeCount_ValueChanged);
connect(model, &PhotoReducerModel::sourceDirectoryValueChanged,
mainWindow, &MainWindow::on_SourceDirectory_Changed);
connect(model, &PhotoReducerModel::targetDirectoryValueChanged,
mainWindow, &MainWindow::on_TargetDirectory_Changed);
}
void SignalRouterController::connectControllerAndModelSignalsToSlots()
{
connect(this, &SignalRouterController::mainWindowReadyForInitialization,
model, &PhotoReducerModel::initializeMainWindow);
connect(this, &SignalRouterController::optionDialogReadyForInitialization,
model, &PhotoReducerModel::initializeOptionsDialog);
connect(this, &SignalRouterController::resizeAllPhotos,
model, &PhotoReducerModel::resizeAllPhotos);
connect(model, &PhotoReducerModel::enableMainWindowResizePhotosButton, this,
&SignalRouterController::enableMainWindowResizePhotosButton);
connect(model, &PhotoReducerModel::acceptOptionsDialog,
this, &SignalRouterController::acceptOptionsDialog);
}
void SignalRouterController::connectControllerAndMainWindowSignalsToSlots()
{
connect(mainWindow, &MainWindow::mainWindowOptionsButtonPressed, this,
&SignalRouterController::mainWindowOptionsButtonPressedCreateOptionsDialog);
connect(mainWindow, &MainWindow::resizeAllPhotos, this,
&SignalRouterController::mainWindowResizePhotosButtonClicked);
connect(this, &SignalRouterController::enablePhotoResizing, mainWindow,
&MainWindow::enableResizePhotosButton);
}
void SignalRouterController::initMainWindowValuesAndShow()
{
emit mainWindowReadyForInitialization();
mainWindow->show();
}
void SignalRouterController::connectModelAndOptionsDialogSignalsToSlots()
{
connectOptionDialogOutToModelIn();
connectModelOutToOptionDialogIn();
}
/*
* Please pardon the massive amount of code repetition in these functions.
* I tried creating tables of signals and slots by examining the contents
* of the QObject header file and using a declaration I found there,
* unfortunately signals and slots are only partially implemented at
* compile time and it generated compilation errors.
*/
void SignalRouterController::connectOptionDialogOutToModelIn()
{
// File Options
connect(optionsDialog, &OptionsDialog::sourceDirectoryLEChanged,
model, &PhotoReducerModel::optionsSourceDirectoryEdited);
connect(optionsDialog, &OptionsDialog::targetDirectoryLEChanged,
model, &PhotoReducerModel::optionsTargetDirectoryEdited);
connect(optionsDialog, &OptionsDialog::optionsJPGFileTypeCheckBoxChanged,
model, &PhotoReducerModel::optionsJPGCheckBoxChanged);
connect(optionsDialog, &OptionsDialog::optionsPNGFileTypecheckBoxChanged,
model, &PhotoReducerModel::optionsPNGCheckBoxChanged);
connect(optionsDialog, &OptionsDialog::optionsSafeWebNameCheckBoxChanged,
model, &PhotoReducerModel::optionsSafeWebNameChanged);
connect(optionsDialog, &OptionsDialog::optionsOverwriteCheckBoxChanged,
model, &PhotoReducerModel::optionsOverWriteFilesChanged);
connect(optionsDialog, &OptionsDialog::optionsaddExtensionLEChanged,
model, &PhotoReducerModel::optionsAddExtensionChanged);
// Photo Options
connect(optionsDialog, &OptionsDialog::optionsMaintainRatioCBChanged,
model, &PhotoReducerModel::optionsMaintainRatioChanged);
connect(optionsDialog, &OptionsDialog::optionsDisplayResizedCBChanged,
model, &PhotoReducerModel::optionsDisplayResizedChanged);
connect(optionsDialog, &OptionsDialog::optionsMaxWidthLEChanged,
model, &PhotoReducerModel::optionsMaxWidthChanged);
connect(optionsDialog, &OptionsDialog::optionsMaxHeightLEChanged,
model, &PhotoReducerModel::optionsMaxHeightChanged);
connect(optionsDialog, &OptionsDialog::optionsScaleFactorLEChanged,
model, &PhotoReducerModel::optionsScaleFactorChanged);
connect(optionsDialog, &OptionsDialog::validateOptionsDialog,
model, &PhotoReducerModel::validateOptionsDialog);
}
void SignalRouterController::connectModelOutToOptionDialogIn()
{
connect(model, &PhotoReducerModel::initOptionsValues, optionsDialog, &OptionsDialog::initOptionsValues);
connect(model, &PhotoReducerModel::modelErrorSignal, optionsDialog, &OptionsDialog::onModelErrorSignal);
connect(model, &PhotoReducerModel::modelClearError, optionsDialog, &OptionsDialog::onModelClearError);
connect(model, &PhotoReducerModel::highlightOverWriteCB, optionsDialog, &OptionsDialog::highlightOverwriteCB);
}
/*
* Slots
*/
void SignalRouterController::mainWindowOptionsButtonPressedCreateOptionsDialog(bool doINeedSignalContents)
{
optionsDialog = new OptionsDialog(mainWindow);
optionsDialog->setObjectName("optionsDialog");
connectModelAndOptionsDialogSignalsToSlots();
emit optionDialogReadyForInitialization();
optionsDialog->show();
}
void SignalRouterController::acceptOptionsDialog()
{
optionsDialog->accept();
}
1 Answer 1
Answers to your questions
Is the code miss using the
QT Signals and Slots
feature?
Uhm, you are using the signals and slots? I don't see an issue here.
During debugging there were some interesting side affects found that created recursion (loosing focus in the DirectoryLineEdit class). Is there any possible bugs in the communications caused by message windows or popup windows?
Not that I know, but I'm not that well versed in Qt. But see below for a minor issue I found.
Are there any QT best practices I am not following?
Qt was created in the mythical time before C++11, when C++ was mostly just C with classes. As such it had to deal with the limitations C++ had, and it has accumulated a lot of workarounds, even things like QString
.
Personally, I think you should focus first on using standard library features in favor of ones from Qt, as long as they work together with Qt of course. Nowadays you can easily write Qt programs without using moc. But opinions might vary.
Show the class destructors be declared as
default
in the header files?
Yes, if they inherit from a class with a virtual destructor, in which case you should also use override
.
Are there any problems with the code format, such as lines too long for the screen?
Personally I am of the opinion that you should just use a code editor that properly wraps lines instead of bikeshedding how long a line should be. Of course, if lines are becoming hundreds of characters long, even that won't help readability anymore. So: keep things concise, but don't try to force every line to fit within some arbitrary limit.
Are there any suggestions on improving the user interface?
There are several improvements you can make. First, the main window is not very useful; you always have to click on "Options" to set all the parameters. Why not start with the options window, then when the user clicks on "Ok", it opens the window with the progress bar and immediately starts resizing. That saves unnecessary confusion and clicks.
Make sure your user interface is accessible. Think:
- Is it usable with a keyboard instead of only the mouse?
- Does it work on very small or very large screens?
- Does it work with all color schemes?
In particular, you cannot make your windows very small, as everything has fixed sizes, and there is no viewport that allows scrolling. Furthermore, I am using a dark theme which has a white text color, and setting the background of NumericLineEdit
to white causes the text to be unreadable.
Trying to be smart and combining many functions into one widget often backfires. The source and target directory fields are QLineEdit
s in the code, and they look like editable text fields on the screen, yet you can't ever edit the filenames manually, instead you are forced to go through the file browser. If you click on say the source directory field, then close the file browser that pops up, then immediately try to click on the source directory field again, it won't do anything because it still has focus. Consider having a completely normal QLineEdit
field, and have a small QButton
next to it labelled "Browse" to open the file browser.
I would also get rid of the QLCDNumber
s, and instead use a regular number field. The LCD font looks silly and outdated, and is less readable than a normal font in my opinion.
Unnecessary member variables
MainWindow
has a lot of pointers to widgets that are only used during object construction time. While it's a minor issue, it's a waste to keep them around. You could, for example, make mwLayout
a local variable, and pass it as a parameter to the helper functions that need it.
Separate the image resizing algorithm from the UI
Even if you use the model-view-viewmodel, you are still binding the image resizing algorithm and the parts of the UI that start the resizing and show the progress too tightly. In particular, your PhotoReducerModel
is conflating the model with the viewmodel. Split it into a pure model that has no Qt code but only knows how to resize images, and a viewmodel that interacts between the UI and the model.
-
\$\begingroup\$ Interesting, I thought I had separated the image resizing algorithm from the model. I thought all the error processing belongs in the model because that is where the business logic is. \$\endgroup\$2025年02月16日 00:34:03 +00:00Commented Feb 16 at 0:34
-
\$\begingroup\$ How do you use the signals and slots without the MOC files? \$\endgroup\$2025年02月16日 00:36:29 +00:00Commented Feb 16 at 0:36
-
\$\begingroup\$ I agree with you about using C++ features first and only using QT for UI. \$\endgroup\$2025年02月16日 00:40:37 +00:00Commented Feb 16 at 0:40
-
\$\begingroup\$ I just reread an SO answer on MVVM and now I understand, I didn't actually do MVVM. \$\endgroup\$2025年02月16日 14:12:38 +00:00Commented Feb 16 at 14:12
-
\$\begingroup\$ You can already connect a signal handler to a slot with
.connect()
, like you do in your code. Creating a slot is harder, but there are tools available for this (a quick search shows github.com/woboq/verdigris for example). Of course, if you only have your own code call the slots you add, you can use any signaling method, not just Qt's. \$\endgroup\$G. Sliepen– G. Sliepen2025年02月16日 21:34:23 +00:00Commented Feb 16 at 21:34
Explore related questions
See similar questions with these tags.