Compare commits

...

3 commits

Author SHA1 Message Date
pls.153
7a4918cc4b update readme 2025-03-21 12:24:44 +01:00
pls.153
121170fcbd add new example 'qsqlite': use SQL from Lisp, load QML images from DB 2025-03-21 12:16:34 +01:00
pls.153
a9d93035b6 correct wrong var name 2025-03-21 12:15:16 +01:00
19 changed files with 598 additions and 7 deletions

View file

@ -171,7 +171,7 @@ QVariant QT::iniDb(const QVariant& vName) {
return vName;
}
QVariant QT::sqlQuery(const QVariant& vQuery, const QVariant& vValues, const QVariant& vRows) {
QVariant QT::sqlQuery(const QVariant& vQuery, const QVariant& vValues, const QVariant& vCols) {
QVariantList results;
QSqlQuery sqlQuery(db);
if (db.open()) {
@ -182,11 +182,11 @@ QVariant QT::sqlQuery(const QVariant& vQuery, const QVariant& vValues, const QVa
sqlQuery.addBindValue(value);
}
if (sqlQuery.exec()) {
auto rows = vRows.toInt();
auto cols = vCols.toInt();
while (sqlQuery.next()) {
if (rows > 1) {
if (cols > 1) {
QVariantList list;
for (auto r = 0; r < rows; r++) {
for (auto r = 0; r < cols; r++) {
list << sqlQuery.value(r);
}
results << QVariant(list);
@ -194,7 +194,7 @@ QVariant QT::sqlQuery(const QVariant& vQuery, const QVariant& vValues, const QVa
results << sqlQuery.value(0);
}
}
if (!rows && query.startsWith("insert", Qt::CaseInsensitive)) {
if (!cols && query.startsWith("insert", Qt::CaseInsensitive)) {
results << sqlQuery.lastInsertId();
}
db.close();

View file

@ -3,9 +3,9 @@
(defvar *file* nil)
(defun query (query &rest values)
(let ((rows (and (x:starts-with "select" query)
(let ((cols (and (x:starts-with "select" query)
(1+ (count #\, (subseq query 0 (search "from" query)))))))
(qrun* (qt:sql-query qt:*cpp* query values rows))))
(qrun* (qt:sql-query qt:*cpp* query values cols))))
(defun ini ()
(setf *file* (app:in-data-path "db"))

9
examples/qsqlite/app.asd Normal file
View file

@ -0,0 +1,9 @@
(defsystem :app
:serial t
:depends-on ()
:components ((:file "lisp/package")
(:file "lisp/qt")
(:file "lisp/ui-vars")
(:file "lisp/db")
(:file "lisp/main")))

127
examples/qsqlite/app.pro Normal file
View file

@ -0,0 +1,127 @@
LISP_FILES = $$files(lisp/*) app.asd make.lisp
exists(/etc/sailfish-release) {
CONFIG += sfos
}
android {
32bit {
ECL = $$(ECL_ANDROID_32)
} else {
ECL = $$(ECL_ANDROID)
}
lisp.commands = $$ECL/../ecl-android-host/bin/ecl \
-norc -shell $$PWD/make.lisp
} else:ios {
lisp.commands = $$(ECL_IOS)/../ecl-ios-host/bin/ecl \
-norc -shell $$PWD/make.lisp
} else:unix {
lisp.commands = /usr/local/bin/ecl -shell $$PWD/make.lisp
} else:win32 {
lisp.commands = ecl.exe -shell $$PWD/make.lisp
}
lisp.input = LISP_FILES
win32: lisp.output = tmp/app.lib
!win32: lisp.output = tmp/libapp.a
QMAKE_EXTRA_COMPILERS += lisp
win32: PRE_TARGETDEPS = tmp/app.lib
!win32: PRE_TARGETDEPS = tmp/libapp.a
QT += quick qml quickcontrols2 sql
TEMPLATE = app
CONFIG += c++17 no_keywords release
DEFINES = DESKTOP_APP INI_LISP QT_EXTENSION
INCLUDEPATH = /usr/local/include
LIBS = -L/usr/local/lib -lecl
DESTDIR = .
TARGET = app
OBJECTS_DIR = tmp
MOC_DIR = tmp
linux: LIBS += -L../../../platforms/linux/lib
macx: LIBS += -L../../../platforms/macos/lib
win32: LIBS += -L../../../platforms/windows/lib
win32 {
include(../../src/windows.pri)
}
android {
DEFINES -= DESKTOP_APP
INCLUDEPATH = $$ECL/include
LIBS = -L$$ECL/lib -lecl
LIBS += -L../../../platforms/android/lib
equals(QT_MAJOR_VERSION, 6) {
QT += core-private
}
lessThan(QT_MAJOR_VERSION, 6) {
QT += androidextras
}
ANDROID_EXTRA_LIBS += $$ECL/lib/libecl.so
ANDROID_PACKAGE_SOURCE_DIR = ../platforms/android
32bit {
ANDROID_ABIS = "armeabi-v7a"
} else {
ANDROID_ABIS = "arm64-v8a"
}
}
ios {
DEFINES -= DESKTOP_APP
INCLUDEPATH = $$(ECL_IOS)/include
LIBS = -L$$(ECL_IOS)/lib -lecl
LIBS += -leclatomic -leclffi -leclgc -leclgmp
LIBS += -L../../../platforms/ios/lib
}
32bit {
android {
equals(QT_MAJOR_VERSION, 6) {
LIBS += -llqml32_armeabi-v7a
}
lessThan(QT_MAJOR_VERSION, 6) {
LIBS += -llqml32
}
}
!android {
LIBS += -llqml32
}
LIBS += -llisp32
} else {
android {
equals(QT_MAJOR_VERSION, 6) {
LIBS += -llqml_arm64-v8a
}
lessThan(QT_MAJOR_VERSION, 6) {
LIBS += -llqml
}
}
!android {
LIBS += -llqml
}
LIBS += -llisp
}
LIBS += -Ltmp -lapp
HEADERS += \
../../src/cpp/main.h \
cpp/qt.h
SOURCES += \
../../src/cpp/main.cpp \
cpp/qt.cpp
RESOURCES += $$files(qml/*)
RESOURCES += $$files(i18n/*.qm)
lupdate_only {
SOURCES += i18n/tr.h
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View file

@ -0,0 +1,94 @@
#include "qt.h"
#include <QSqlQuery>
#include <QSqlError>
#include <QQuickView>
#include <QtDebug>
QObject* ini() {
static QObject* qt = nullptr;
if (qt == nullptr) {
qt = new QT;
}
return qt;
}
QT::QT() : QObject() {
}
QVariant QT::dataPath(const QVariant& prefix) {
// for desktop
QString path = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation);
path.truncate(path.lastIndexOf(QChar('/')));
path.append(QStringLiteral("/lqml-qsqlite/") + prefix.toString());
return path;
}
// SQL
QVariant QT::iniDb(const QVariant& vName, const QVariant& vQuickView) {
// add database image provider, in order to load images in QML directly from a SQL database
auto quickView = vQuickView.value<QQuickView*>();
quickView->engine()->addImageProvider(QLatin1String("db"), new DatabaseImageProvider(this));
// ini
db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName(vName.toString());
return vName;
}
QVariant QT::sqlQuery(const QVariant& vQuery, const QVariant& vValues, const QVariant& vCols) {
QVariantList results;
QSqlQuery sqlQuery(db);
if (db.open()) {
QString query = vQuery.toString();
sqlQuery.prepare(vQuery.toString());
const QVariantList values = vValues.value<QVariantList>();
for (auto value : values) {
sqlQuery.addBindValue(value);
}
if (sqlQuery.exec()) {
auto cols = vCols.toInt();
while (sqlQuery.next()) {
if (cols > 1) {
QVariantList list;
for (auto r = 0; r < cols; r++) {
list << sqlQuery.value(r);
}
results << QVariant(list);
} else {
results << sqlQuery.value(0);
}
}
if (!cols && query.startsWith("insert", Qt::CaseInsensitive)) {
results << sqlQuery.lastInsertId();
}
db.close();
return results;
}
db.close();
}
QString text;
if (sqlQuery.lastError().isValid()) {
text = sqlQuery.lastError().text();
} else {
text = db.lastError().text();
}
qDebug() << "SQL error:" << text;
return QVariant();
}
// database image provider
QPixmap DatabaseImageProvider::requestPixmap(const QString& name, QSize* size, const QSize& requestedSize) {
auto result = qt->sqlQuery(
"select data from images where name = ?",
QVariantList() << name,
1); // number of returned columns
QPixmap pixmap;
pixmap.loadFromData(result.value<QVariantList>().first().toByteArray());
*size = pixmap.size();
if (requestedSize.isValid() && (pixmap.size() != requestedSize)) {
pixmap = pixmap.scaled(requestedSize);
}
return pixmap;
}

36
examples/qsqlite/cpp/qt.h Normal file
View file

@ -0,0 +1,36 @@
#pragma once
#include <QtCore>
#include <QSqlDatabase>
#include <QQuickImageProvider>
#ifdef Q_CC_MSVC
#define LIB_EXPORT __declspec(dllexport)
#else
#define LIB_EXPORT
#endif
extern "C" { LIB_EXPORT QObject* ini(); }
class QT : public QObject {
Q_OBJECT
public:
Q_INVOKABLE QVariant dataPath(const QVariant&);
Q_INVOKABLE QVariant iniDb(const QVariant&, const QVariant&);
Q_INVOKABLE QVariant sqlQuery(const QVariant&, const QVariant&, const QVariant&);
QT();
QSqlDatabase db;
};
class DatabaseImageProvider : public QQuickImageProvider {
public:
DatabaseImageProvider(QT* _qt) : QQuickImageProvider(QQuickImageProvider::Pixmap), qt(_qt) {}
QPixmap requestPixmap(const QString&, QSize*, const QSize&);
QT* qt;
};

View file

@ -0,0 +1,27 @@
QT += sql quick
TEMPLATE = lib
CONFIG += c++17 plugin release no_keywords
DEFINES += PLUGIN
INCLUDEPATH = /usr/local/include ../../../src/cpp
LIBS = -L/usr/local/lib -lecl
DESTDIR = ./
TARGET = qt
OBJECTS_DIR = ./tmp/
MOC_DIR = ./tmp/
HEADERS += qt.h
SOURCES += qt.cpp
linux {
LIBS += -L../../../platforms/linux/lib
}
macx {
LIBS += -L../../../platforms/macos/lib
}
win32 {
include(../../../src/windows.pri)
LIBS += -L../../../platforms/windows/lib
}

View file

@ -0,0 +1,34 @@
(in-package :db)
(defvar *file* nil)
(defun query (query &rest values)
(let ((rows (and (x:starts-with "select" query)
(1+ (count #\, (subseq query 0 (search "from" query)))))))
(qrun* (qt:sql-query qt:*cpp* query values rows))))
(defun ini ()
(setf *file* (app:in-data-path "db"))
(ensure-directories-exist *file*)
(qt:ini-db qt:*cpp* (namestring *file*) *quick-view*)
(query "create table if not exists images (id integer primary key autoincrement, name text, data blob)"))
(defun size ()
(first (query "select count(*) from images")))
(defun save-image (name data)
"Inserts image NAME, DATA (vector of octets) and returns the new image ID."
(first (query "insert into images (name, data) values (?, ?)"
name data)))
(defun load-images ()
(query "select name, data from images order by id"))
(defun delete-image (id)
(query "delete from images where id = ?"
id)
(values))
(defun delete-all-images ()
(query "delete from images"))

View file

@ -0,0 +1,35 @@
(in-package :app)
(defun ini ()
(qt:ini)
(db:ini)
(populate-db)
;; loads image directly from database, see 'cpp/qt.cpp::DatabaseImageProvider'
(q> |source| ui:*logo* "image://db/logo-128"))
(defun in-data-path (&optional (file "") (prefix "data/"))
#+mobile
(merge-pathnames (x:cc prefix file))
#-mobile
(x:cc (qrun* (qt:data-path qt:*cpp* prefix)) file))
(defun file-bytes (file)
(with-open-file (s file :element-type '(unsigned-byte 8))
(let ((arr (make-array (file-length s) :element-type '(unsigned-byte 8))))
(read-sequence arr s)
arr)))
;; put images in database
(defun populate-db ()
(let ((files (sort (directory (merge-pathnames "blob/*.png"))
'string< :key 'pathname-name)))
(when (/= (length files) (db:size))
(if (probe-file (merge-pathnames "blob/"))
(progn
(db:delete-all-images)
(dolist (file files)
(db:save-image (pathname-name file) (file-bytes file)))))
(x:d "No 'blob/' directory found."))))
(qlater 'ini)

View file

@ -0,0 +1,15 @@
(defpackage :app
(:use :cl :qml)
(:export
#:in-data-path))
(defpackage :db
(:use :cl :qml)
(:export
#:delete-image
#:delete-all-images
#:ini
#:load-images
#:save-image
#:size))

View file

@ -0,0 +1,19 @@
(defpackage :qt
(:use :cl :qml)
(:export
#:*cpp*
#:data-path
#:ini
#:ini-db
#:sql-query))
(in-package :qt)
(defvar *cpp* nil)
(defun ini ()
(setf *cpp*
#+qt-plugin (qload-c++ "cpp/qt")
#-qt-plugin (qfind-child nil "QT"))
(let ((*package* (find-package :qt)))
(define-qt-wrappers *cpp*)))

View file

@ -0,0 +1,9 @@
(defpackage ui
(:use :cl :qml)
(:export
#:*logo*))
(in-package :ui)
(defparameter *logo* "logo")

View file

@ -0,0 +1,88 @@
;; check target
(let ((arg (first (ext:command-args))))
(mapc (lambda (name feature)
(when (search name arg)
(pushnew feature *features*)))
(list "/ecl-android" "/ecl-ios")
(list :android :ios)))
#+(or android ios)
(pushnew :mobile *features*)
(when (probe-file "/etc/sailfish-release")
(pushnew :sfos *features*))
(defun cc (&rest args)
(apply 'concatenate 'string args))
#+mobile
(defvar *assets* #+android "../platforms/android/assets/lib/"
#+ios "../platforms/ios/assets/Library/")
#+mobile
(defun shell (command)
(ext:run-program "sh" (list "-c" command)))
#+mobile
(let ((blob #+android *assets*
#+ios (cc *assets* "../Documents/")))
(ensure-directories-exist blob)
(shell (cc "cp -r ../blob " blob)))
(require :asdf)
(require :cmp)
(push (merge-pathnames "../")
asdf:*central-registry*)
(setf *default-pathname-defaults*
(truename (merge-pathnames "../../../"))) ; LQML root
(defvar *current*
(let ((name (namestring *load-truename*)))
(subseq name
(length (namestring *default-pathname-defaults*))
(1+ (position #\/ name :from-end t)))))
(dolist (file (list "package" "x" "ecl-ext" "ini" "qml")) ; load LQML symbols
(load (merge-pathnames file "src/lisp/")))
(defun cc (&rest args)
(apply 'concatenate 'string args))
(progn
(defvar cl-user::*tr-path* (truename (cc *current* "i18n/")))
(load "src/lisp/tr"))
#-mobile
(asdf:make-build "app"
:monolithic t
:type :static-library
:move-here (cc *current* "build/tmp/")
:init-name "ini_app")
#+mobile
(progn
(pushnew :interpreter *features*)
(defvar *asdf-system* "app")
(defvar *ql-libs* (cc *current* "ql-libs.lisp"))
(defvar *init-name* "ini_app")
(defvar *library-path* (format nil "~Abuild-~A/tmp/"
*current*
#+android "android"
#+ios "ios"))
(defvar *require* (list :ecl-curl))
(load "platforms/shared/make"))
;; rename lib
(let* ((from #-mobile (cc *current* (format nil "build/tmp/app--all-systems.~A"
#+msvc "lib"
#-msvc "a"))
#+mobile (cc *library-path* "app--all-systems.a"))
(to #-msvc "libapp.a"
#+msvc "app.lib")
(to* #-mobile (cc *current* "build/tmp/" to)
#+mobile (cc *library-path* to)))
(when (probe-file to*)
(delete-file to*))
(rename-file from to))

View file

@ -0,0 +1,12 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
Item {
width: 300
height: 500
Image {
objectName: "logo"
anchors.centerIn: parent
}
}

View file

@ -0,0 +1,57 @@
Info
----
This is a simple example of:
* using the **qsqlite** (Qt specific SQLite DB) library directly from Lisp
* providing a custom image provider, so we can directly load images in QML from
an SQL database
The qsqlite library that comes with Qt has the advantage of being pulled in
automatically as a dependency, and behaving exactly the same, no matter what OS
is used. This is especially convenient on mobile.
If you run the example with:
```
$ lqml run.lisp -slime
```
you can then try to change the image source from the REPL:
```
(in-package :app)
(q> |source| ui:*logo* "image://db/logo-256")
```
This will load the image directly from the database.
Prepare
-------
Please copy the app template files first:
```
$ cd ..
$ ./copy.sh qsqlite
```
For running on the desktop, you'll also need to compile the plugin:
```
$ cd cpp
$ qmake; make
```
Run
---
```
lqml run.lisp
```
Optionally pass `-slime` to start a Swank server, and connect from Emacs with
`M-x slime-connect`.
During development you can pass `-auto`, which will reload all QML files after
you made a change to any of them and saved it. For re-initialization after
reloading, file `lisp/qml-reload/on-reloaded` will be loaded.
Closing the window quits the app. If you try to kill it with `ctrl-c`, you need
an additional `ctrl-d` to exit from ECL. To quit from Slime, do `(qq)` which is
short for `(qquit)`.

29
examples/qsqlite/run.lisp Normal file
View file

@ -0,0 +1,29 @@
(in-package :qml-user)
(pushnew :qt-plugin *features*)
(require :asdf)
(push (merge-pathnames "./")
asdf:*central-registry*)
(asdf:operate 'asdf:load-source-op :app)
(qset *quick-view*
|x| 75
|y| 75)
(defun option (name)
(find name (ext:command-args) :test 'search))
;;; trivial auto reload of all QML files after saving any change
(when (option "-auto")
(load "lisp/qml-reload/auto-reload"))
;;; for Slime after copying 'lqml-start-swank.lisp' from LQML sources
;;; to your Slime directory, which is assumed to be '~/slime/'
(when (option "-slime")
(load "~/slime/lqml-start-swank")) ; for 'slime-connect' from Emacs