2
\$\begingroup\$

I have written a simple app that generates a menu in the form of a list of recipes. It furthermore generates a shopping list, given a menu, consisting of the (unique) ingredients in the recipes that make up the menu.

A database provides a list of all possible recipes and the related ingredients. Here is its structure:

recipe table:

id (P) name
1 Beef stew
... ...

ingredient table:

id (P) name
1 Beef
2 Red wine
... ...

recipeIngredient table:

recipe_id (F) ingredient_id (F)
1 1
1 2
... ...

The app opens and interacts with the database in a C++ class, and shows the user the menu/list with QML code. I am fairly new to both QML and databases. looking for general feedback on the design of this software, feel free to be as critical as you feel necessary. I also have some detailed questions:

  • I am using a simple SQLite3 database for my application. From a bit of research I have done, I understand the Qt resource system cannot handle database files directly (see for example this), as that would pass the qrc URI to the SQlite driver, which would then be unable to locate the file. Furthermore, resource files are read-only. What is then the best practice for shipping a database with an application? Is packaging the database with the app and installing it to a known location together with the app the way to go? I would do that by configuring the CMake installer in CMakeLists.txt with something like install(FILES "${CMAKE_SOURCE_DIR}/res/recipes.db" DESTINATION "${CMAKE_INSTALL_DATADIR}), and then try to locate that and open it from DBManager.cpp. But I suppose this is not possible when cross-compiling for another platform, e.g. when packaging an apk for android. Another strategy I encountered for solving this problem is to declare the database file as a resource and then copying it to an accessible location at runtime (see main.cpp), using QStandardPaths. My understanding is that the database would then exist, at any given time, both in the apk as a resource file and somewhere else on the target system, which seems like a waste of space and could potentially make the apk very large if the database is extended.

  • Am I accessing the database in a sensible way? Supposing this will scale to a large database with many recipes, should I be concerned with some specific metric when I design my queries (e.g. minimizing the number of queries)?

  • It seems to me there is some repetition of code in the QML files, am I not using some patterns that I should instead employ?

  • Is my way of controlling QML elements with buttons sensible or am I missing some easier way of making the buttons affect what goes on on the screen?

main.cpp:

#include <QFile>
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QStandardPaths>

#include "DBManager.h"

int main(int argc, char *argv[])
{
    // Supposing I copy the file at runtime
    QFile::copy(":/databases/ricettario.db", QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/ricettario.db");

    QGuiApplication app(argc, argv);

    QQmlApplicationEngine engine;
    const QUrl url(u"qrc:/MenuHelper/Main.qml"_qs);
    QObject::connect(
        &engine,
        &QQmlApplicationEngine::objectCreationFailed,
        &app,
        []() { QCoreApplication::exit(-1); },
        Qt::QueuedConnection);

    DBManager* recipeDBManager = new DBManager();
    qmlRegisterSingletonInstance("MenuHelper", 1, 0, "DBManager", recipeDBManager);

    engine.load(url);

    return app.exec();
}

DBManager.h:

#pragma once

#include <QObject>
#include <QtSql/QSqlDatabase>
#include <QtQml/qqmlregistration.h>

class DBManager : public QObject
{
    Q_OBJECT
    QML_ELEMENT
    QML_SINGLETON

public:
    explicit DBManager(QObject *parent = nullptr);
    ~DBManager();

    Q_PROPERTY(QStringList menuRecipes  READ menuRecipes    NOTIFY menuRecipesChanged)
    Q_PROPERTY(QStringList shoppingList READ shoppingList   NOTIFY shoppingListChanged)

    Q_INVOKABLE void generateMenu();
    Q_INVOKABLE void generateShoppingList();
    Q_INVOKABLE void swapRecipe(int index);
    Q_INVOKABLE void deleteRecipe(int index);
    Q_INVOKABLE void addRecipe();
    Q_INVOKABLE void deleteIngredient(int index);

    QStringList menuRecipes() const { return _menuRecipes; }
    QStringList shoppingList() const { return _shoppingList; }


signals:
    void menuRecipesChanged();
    void shoppingListChanged();

private:
    QString _findNewRecipe() const;

    QSqlDatabase _recipeDatabaseConnection;
    QStringList _menuRecipes, _shoppingList;
};

DBManager.cpp:

#include "DBManager.h"

#include <QDebug>
#include <QSqlError>
#include <QSqlQuery>
#include <QStandardPaths>
#include <fstream>
#include <string>
#include <iostream>

DBManager::DBManager(QObject *parent)
    : QObject{parent}
    , _recipeDatabaseConnection(QSqlDatabase::addDatabase("QSQLITE"))
{
    _recipeDatabaseConnection.setDatabaseName(QStandardPaths::locate(QStandardPaths::AppDataLocation, "ricettario.db"));
    if(!_recipeDatabaseConnection.open()){
        qWarning() << _recipeDatabaseConnection.lastError().text();
    } else {
        qDebug() << "Succesfully opened" << _recipeDatabaseConnection.databaseName() << "for reading";
    }
}

DBManager::~DBManager()
{
    _recipeDatabaseConnection.close();
}

void DBManager::generateMenu()
{
    QSqlQuery query;
    query.exec("SELECT name FROM recipe ORDER BY RANDOM() LIMIT 3");
    _menuRecipes.clear();
    while(query.next()){
        _menuRecipes.append(query.value(0).toString());
    }
    emit menuRecipesChanged();
}

void DBManager::swapRecipe(int index)
{
    QString newRecipe = _findNewRecipe();
    if (!newRecipe.isEmpty()){
        _menuRecipes[index] = newRecipe;
        emit menuRecipesChanged();
    }
}

void DBManager::deleteRecipe(int index)
{
    _menuRecipes.removeAt(index);
    emit menuRecipesChanged();
}

void DBManager::addRecipe()
{
    QString newRecipe = _findNewRecipe();
    if (!newRecipe.isEmpty()){
        _menuRecipes.append(newRecipe);
        emit menuRecipesChanged();
    }
}

void DBManager::deleteIngredient(int index)
{
    _shoppingList.removeAt(index);
    emit shoppingListChanged();
}

QString DBManager::_findNewRecipe() const
{
    QSqlQuery query;
    query.exec("SELECT name FROM recipe "
               "WHERE name NOT IN (\"" + _menuRecipes.join("\", \"") + "\") "
                                               "ORDER BY RANDOM() LIMIT 1 ");
    if(query.next()){
        return query.value(0).toString();
    } else {
        qWarning() << "Could not find another recipe";
        return QString();
    }
}

void DBManager::generateShoppingList()
{
    _shoppingList.clear();
    QSqlQuery query;
    query.exec("SELECT DISTINCT ingredient.name FROM recipe "
               "JOIN recipe_ingredient ON recipe.id = recipe_ingredient.recipe_id "
               "JOIN ingredient ON recipe_ingredient.ingredient_id = ingredient.id "
               "WHERE recipe.name IN (\"" + _menuRecipes.join("\", \"") + "\")");
    while(query.next()){
        _shoppingList.append(query.value(0).toString());
    }
    emit shoppingListChanged();
}

main.qml:

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import MenuHelper

Window {
    width: 400
    height: 800
    visible: true
    title: qsTr("Menu Helper")

    color: MyPalette.secondaryColor

    TopBanner{
        id:             topBanner
        titleText:      qsTr("Menu")
        anchors.top:    parent.top
        anchors.left:   parent.left
        anchors.right:  parent.right
        height:         ScreenSizes.defaultFontPixelHeight * 8
    }

    MenuListArea{
        id:             recipesList
        anchors.top:    topBanner.bottom
        anchors.left:   parent.left
        anchors.right:  parent.right
        anchors.bottom: buttonsBar.top
    }

    ShoppingListArea{
        id:             shoppingList
        anchors.top:    topBanner.bottom
        anchors.left:   parent.left
        anchors.right:  parent.right
        anchors.bottom: buttonsBar.top
        visible:        !recipesList.visible
    }

    Rectangle {
        id:                 buttonsBar
        anchors.left:       parent.left
        anchors.right:      parent.right
        anchors.bottom:     parent.bottom
        height:             ScreenSizes.defaultFontPixelHeight * 10
        color:          MyPalette.tertiaryColor

        RowLayout{
            anchors.centerIn:   parent
            spacing:            ScreenSizes.defaultFontPixelHeight * 2

            Button {
                id:         refreshButton
                text:       recipesList.visible ? qsTr("Generate new menu") : qsTr("Back to menu")
                onClicked:  {
                    if (recipesList.visible){
                        DBManager.generateMenu()
                    } else {
                        recipesList.visible = true
                        topBanner.titleText = qsTr("Menu")
                    }
                }
            }

            Button {
                id:         shoppingListButton
                text:       qsTr("To list")
                onClicked:  {
                    DBManager.generateShoppingList()
                    recipesList.visible = false
                    topBanner.titleText = qsTr("Shopping List")
                }
            }
        }
    }
}

MenuListArea.qml:

import QtQuick 6.4
import QtQuick.Controls
import QtQuick.Layouts
import MenuHelper

Rectangle {
    id:                 _root
    anchors.margins:    ScreenSizes.defaultFontPixelHeight * 2

    Component {
        id:             recipeComponent

        RowLayout {
            width:      _root.width

            Text {
                Layout.alignment:   Qt.AlignLeft
                Layout.fillWidth:   true
                text:               modelData
                wrapMode:           Text.WordWrap
            }

            RowLayout {
                Layout.alignment:   Qt.AlignRight

                Button {
                    icon.source:        "qrc:/icons/swap.svg"
                    icon.color:         MyPalette.secondaryColor
                    icon.width:         ScreenSizes.defaultFontPixelHeight * 2
                    icon.height:        ScreenSizes.defaultFontPixelHeight * 2
                    implicitWidth:      icon.width + padding * 2
                    onClicked:          DBManager.swapRecipe(index)
                }

                Button {
                    icon.source:        "qrc:/icons/delete.svg"
                    icon.color:         MyPalette.secondaryColor
                    icon.width:         ScreenSizes.defaultFontPixelHeight * 2
                    icon.height:        ScreenSizes.defaultFontPixelHeight * 2
                    implicitWidth:      icon.width + padding * 2
                    onClicked:          DBManager.deleteRecipe(index)
                }
            }
        }
    }

    ListView {
        id:             recipeList
        model:          DBManager.menuRecipes
        delegate:       recipeComponent
        height:         contentHeight
    }

    Button {
        anchors.right:      parent.right
        anchors.top:        recipeList.bottom
        icon.source:        "qrc:/icons/add.svg"
        icon.color:         MyPalette.secondaryColor
        icon.width:         ScreenSizes.defaultFontPixelHeight * 2
        icon.height:        ScreenSizes.defaultFontPixelHeight * 2
        implicitWidth:      icon.width + padding * 2
        onClicked:          DBManager.addRecipe()
    }
}

ShoppingListArea.qml:

import QtQuick 6.4
import QtQuick.Controls
import QtQuick.Layouts
import MenuHelper

Rectangle {
    id:                 _root
    anchors.margins:    ScreenSizes.defaultFontPixelHeight * 2

    Component {
        id:             recipeComponent
        RowLayout {
            width:      _root.width

            Text {
                Layout.alignment:   Qt.AlignLeft
                text:               modelData
            }

            Button {
                Layout.alignment:   Qt.AlignRight
                icon.source:        "qrc:/icons/delete.svg"
                icon.color:         MyPalette.secondaryColor
                icon.width:         ScreenSizes.defaultFontPixelHeight * 2
                icon.height:        ScreenSizes.defaultFontPixelHeight * 2
                implicitWidth:      icon.width + padding * 2
                onClicked:          DBManager.deleteIngredient(index)
            }
        }
    }

    ListView {
        id:             recipeList
        model:          DBManager.shoppingList
        delegate:       recipeComponent
        anchors.fill:   parent
    }
}


TopBanner.qml:

import QtQuick 6.4

Item {
    property string titleText
    Rectangle {
        id:                 background
        anchors.fill:       parent
        color:              MyPalette.primaryColor
    }

    Text {
        anchors.centerIn:   background
        text:               titleText
        color:              MyPalette.secondaryColor
        font.pointSize:     ScreenSizes.extraLargeFontPointSize
    }
}

\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.