Add a very ugly login page to test database connection

I want to perform all SQL queries in a thread, to avoid freezing the UI,
that sometimes might happen when there is a lot of data to fetch; should
not happen very often, though.

Neither libpq nor Qt SQL allow queries on the same connection from
differents threads, and, in Qt SQL, all queries must be performed from
the same thread where the connection was established.  In Qt5 i had to
either create a connection per thread, or use a QThread-derived object
to hold the connection and use signals and slots to pass query and
response data between the UI and database threads; it was usable but not
pretty.

With Qt6 and Concurrent’s QThreadPool now i can use QFutures instead,
that are not as cumbersome as with Qt5, because i no longer need
QFutureWatcher.  I still have the problem that all queries must be done
from within the same thread, and QThreadPool uses an arbitrary thread.
The solution is to create a “pool” with a single, non-expirable thread,
and call all Concurrent::run onto that pool.

I have to test it properly, and first need to open the database to test
whether that, at least, works. I added a simple “login page” for that,
and to make a first attempt to error messages; i use a control that is
like Kirigami’s InlineMessage for now, but i am not sure.

I also do not know how i will configure database’s connection details. I
usually make use of pg_service.conf, because then the application only
need to know its service name, but i am not sure whether other people
would find it as comfortable as i do.
This commit is contained in:
jordi fita mas 2024-12-16 12:59:19 +01:00
parent 49b2c035ad
commit 07705b012a
7 changed files with 274 additions and 2 deletions

View File

@ -5,8 +5,10 @@ project(camper VERSION 0.1 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Qt6 REQUIRED COMPONENTS find_package(Qt6 REQUIRED COMPONENTS
Concurrent
Quick Quick
QuickControls2 QuickControls2
Sql
) )
qt_standard_project_setup(REQUIRES 6.5) qt_standard_project_setup(REQUIRES 6.5)

View File

@ -6,8 +6,12 @@ qt_add_qml_module(${PROJECT_NAME}
URI Camper URI Camper
VERSION 1.0 VERSION 1.0
DEPENDENCIES QtCore DEPENDENCIES QtCore
SOURCES
database.cpp database.h
QML_FILES QML_FILES
ErrorNotification.qml
Main.qml Main.qml
SelectableLabel.qml
) )
set_target_properties(${PROJECT_NAME} PROPERTIES set_target_properties(${PROJECT_NAME} PROPERTIES
@ -19,8 +23,11 @@ set_target_properties(${PROJECT_NAME} PROPERTIES
) )
target_link_libraries(${PROJECT_NAME} target_link_libraries(${PROJECT_NAME}
PRIVATE Qt6::Quick PRIVATE
PRIVATE Qt6::QuickControls2 Qt6::Concurrent
Qt6::Quick
Qt6::QuickControls2
Qt6::Sql
) )
include(GNUInstallDirs) include(GNUInstallDirs)

117
src/ErrorNotification.qml Normal file
View File

@ -0,0 +1,117 @@
import QtQuick
import QtQuick.Controls
Control {
id: control
property alias text: label.text
function show(errorMessage: string) {
control.text = errorMessage;
control.visible = true;
hideTimer.start();
}
Accessible.ignored: !visible
Accessible.role: Accessible.AlertMessage
implicitHeight: visible ? (contentLayout.implicitHeight + topPadding + bottomPadding) : 0
opacity: visible ? 1 : 0
padding: 4
visible: false
background: Rectangle {
id: borderRect
border.color: "#da4453"
color: "#ebced2"
radius: 5
}
contentItem: Item {
id: contentLayout
Accessible.ignored: true
implicitHeight: Math.max(label.implicitHeight, closeButton.implicitHeight)
Behavior on opacity {
enabled: control.visible
NumberAnimation {
duration: 200
}
}
SelectableLabel {
id: label
Accessible.ignored: !control.visible
anchors {
left: parent.left
leftMargin: 4
right: closeButton.left
rightMargin: 4
top: parent.top
}
}
ToolButton {
id: closeButton
Accessible.ignored: !control.visible
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
display: ToolButton.IconOnly
height: implicitHeight
icon.name: "dialog-close"
text: qsTr("Close")
onClicked: function () {
control.visible = false;
}
}
}
Behavior on implicitHeight {
enabled: !control.visible
NumberAnimation {
duration: 200
}
}
Behavior on opacity {
enabled: !control.visible
NumberAnimation {
duration: 200
}
}
onImplicitHeightChanged: function () {
height = implicitHeight;
}
onOpacityChanged: function () {
if (opacity === 0) {
contentLayout.opacity = 0;
} else if (opacity === 1) {
contentLayout.opacity = 1;
}
}
anchors {
bottom: parent.bottom
bottomMargin: 8
left: parent.left
margins: 18 * 4
right: parent.right
}
Timer {
id: hideTimer
interval: 10000
repeat: false
onTriggered: function () {
control.visible = false;
}
}
}

View File

@ -1,9 +1,80 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts
import Camper
ApplicationWindow { ApplicationWindow {
height: 480 height: 480
title: qsTr("Camper") title: qsTr("Camper")
visible: true visible: true
width: 640 width: 640
ColumnLayout {
Label {
text: qsTr("&User:")
}
TextField {
id: user
focus: true
validator: RegularExpressionValidator {
regularExpression: /[^s].*/
}
onAccepted: function () {
loginAction.trigger();
}
}
Label {
text: qsTr("&Password:")
}
TextField {
id: password
echoMode: TextInput.Password
onAccepted: function () {
loginAction.trigger();
}
}
Button {
action: loginAction
}
}
ErrorNotification {
id: errorNotification
anchors {
bottom: parent.bottom
bottomMargin: 8
left: parent.left
margins: 18 * 4
right: parent.right
}
}
Action {
id: loginAction
enabled: user.acceptableInput
text: "&Login"
onTriggered: function () {
Database.open(user.text, password.text);
}
}
Connections {
function onErrorOcurred(errorMessage) {
errorNotification.show(errorMessage);
}
target: Database
}
} }

21
src/SelectableLabel.qml Normal file
View File

@ -0,0 +1,21 @@
import QtQuick
import QtQuick.Controls
Control {
id: control
property alias text: textArea.text
contentItem: TextArea {
id: textArea
padding: 0
readOnly: true
selectByMouse: true
wrapMode: Text.WordWrap
HoverHandler {
cursorShape: Qt.IBeamCursor
}
}
}

27
src/database.cpp Normal file
View File

@ -0,0 +1,27 @@
#include "database.h"
#include <QSqlDatabase>
#include <QSqlError>
#include <QtConcurrent>
Database::Database(QObject *parent)
: QObject{parent}
, m_pool{}
{
m_pool.setMaxThreadCount(1);
m_pool.setExpiryTimeout(-1);
}
QFuture<void> Database::open(const QString &user, const QString &password)
{
return QtConcurrent::run(&m_pool, [this, user, password]() {
QString connectionName("main");
QSqlDatabase db = QSqlDatabase::addDatabase("QPSQL", connectionName);
db.setConnectOptions("service=camper; options=-csearch_path=camper,public");
if (!db.open(user, password)) {
const QString errorMessage(db.lastError().text());
db = QSqlDatabase(); // Otherwise removeDatabase complains is still being used.
QSqlDatabase::removeDatabase(connectionName);
emit errorOcurred(errorMessage);
}
});
}

27
src/database.h Normal file
View File

@ -0,0 +1,27 @@
#ifndef DATABASE_H
#define DATABASE_H
#include <QFuture>
#include <QObject>
#include <QThreadPool>
#include <QtQmlIntegration>
class Database : public QObject
{
Q_OBJECT
QML_SINGLETON
QML_ELEMENT
public:
explicit Database(QObject *parent = nullptr);
Q_INVOKABLE QFuture<void> open(const QString &user, const QString &password);
signals:
void errorOcurred(const QString &errorMessage);
private:
QThreadPool m_pool;
};
#endif // DATABASE_H