Меnu:


La aplicación que hoy portaremos a commonQt en un aplicativo de administración de notas, nos permite crear notas y luego buscar con facilidad cualquier termino dentro de todo el conjunto de notas creadas. Este aplicativo usa por debajo sqlite.

Porque commonQt y no Qt solamente?

Risc es el futuro. Es una frase de la película Hackers de 1995. Bueno ya hace varios años que tenemos ARM que es un retoño de Risc. Comenzamos en el año 2007 con la tableta Nokia N800, hace uno tres de años (era 2018) nos mudamos al teléfono Nokia N900 y hace un par de años que tenemos a disposición una maquina permanentemente encendida que es la orange pi. Uno de los aplicativos que aun no hemos podido migrar de x86 a ARM es el dbdesigner-fork. Es para modelar bases de datos esta hecho en kylix (Delphi para GNU/Linux), ya hace muchos años que no se usa el kylix. Sin embargo el dbdesigner sigue funcionando, por lo tanto lo seguimos usando. Han habido intentos de migrarlo al Lazarus pero sin éxito hasta hoy. Y como nuestro editor de texto favorito es el Emacs, nos pareció un buen momento para hacer un pequeño aplicativo en commonQt con la mira de en un futuro cercano codificar una alternativa al dbdesigner en commonQt. Por el tamaño del dbdesigner, esperamos que el commonQt nos acelere el desarrollo. Ya veremos como nos va.

Que es lo que haremos

Portaremos el app hecho en wxwidgets a commonQt. Usaremos en ArchLinux el designer de Qt para diseñar la interfaz de usuario.

Anteriormente ya hemos escrito acerca de como hacer el aplicativo en 2 artículos:

  1. Ejemplo faltante en wxwidgets.
  2. Ejemplo faltante en wxwidgets parte 2.

Mostrando el app en wxwidgets

Mostrando el código common lisp

(require 'qt)
(in-package :common-lisp-user)

;;; adding sqlite here too cos connect and disconnect gave "name-conflict"
(defpackage "PEAPP-QT"
  (:use cl qt sqlite)
  (:shadowing-import-from :sqlite "CONNECT" "DISCONNECT")
  (:export "TIMELINE-MAIN"))

(in-package :peapp-qt)
(defvar *entity-note* nil)
(defvar *db* (sqlite:connect "/home/user/.wxtimeline/wx_timelines.sqlite3")) ;;Connect to the sqlite database.
(named-readtables:in-readtable :qt)

(defun find-child (object name)
  (let ((children (#_children object)))
    (or
     (loop for child in children
           when (equal name (#_objectName child))
           return child)
     (loop for child in children
           thereis (find-child child name)))))

(defclass timeline-mainform ()
  (
   (tableWidget :accessor tableWidget)
   (lineEdit :accessor lineEdit)
   (detailDialog :accessor detailDialog)
   )
  (:metaclass qt-class)
  (:qt-superclass "QWidget")
  (:slots ("timeline-mainform-show-dialog-detail()" timeline-mainform-show-dialog-detail)
          ("timeline-mainform-selected-item()" timeline-mainform-selected-item)
          ("timeline-mainform-on-add()" timeline-mainform-on-add)
          ("timeline-mainform-on-delete()" timeline-mainform-on-delete)
          ("timeline-mainform-on-ok()" timeline-mainform-on-ok)
          ("timeline-mainform-line-edit-text-changed()" timeline-mainform-line-edit-text-changed))
  (:override ("closeEvent" timeline-mainform-close-event)))

(defmethod initialize-instance :after ((instance timeline-mainform) &key)
  (new instance)
  (#_setWindowTitle instance "timeline on commonQt [uses qt4.8.7]")
  (with-objects ((file (#_new QFile "/home/user/dev/cl/timeline/timeline_main_form_widget.ui"))
                 (loader (#_new QUiLoader)))
    (if (#_open file 1)
        (let ((win (#_load loader file instance))
              (layout (#_new QVBoxLayout))
              )
          (#_close file)
          (#_addWidget layout win)
          (#_setLayout instance layout)
          (setf (tableWidget instance) (find-child win "tableWidget"))
          (setf (lineEdit instance) (find-child win "lineEdit"))
          (#_setBuddy (find-child win "label") (find-child win "lineEdit"))
          (qt:connect (find-child win "pushButton") "clicked()" instance "timeline-mainform-on-add()");add
          (qt:connect (find-child win "pushButton_2") "clicked()" instance "close()");cancel
          (qt:connect (find-child win "pushButton_3") "clicked()" instance "timeline-mainform-on-delete()");del
          (qt:connect (find-child win "pushButton_4") "clicked()" instance "timeline-mainform-on-ok()");ok
          (qt:connect (find-child win "tableWidget") "cellActivated(int, int)" instance "timeline-mainform-selected-item()")
          (qt:connect (find-child win "lineEdit") "textChanged(QString)" instance "timeline-mainform-line-edit-text-changed()")
          (timeline-mainform-set-ui-properties instance)
          (timeline-mainform-ui-load-data instance)
          )
        (error (concatenate 'string "Couldn't open " "filename" " file!") ))))

(defmethod timeline-mainform-set-ui-properties ((instance timeline-mainform))
  (#_setColumnCount (tableWidget instance) 2) ;;; just two columns {forget the third-one}
  (#_setHorizontalHeaderLabels (tableWidget instance) '("DateDesc" "ID")) ;;; {rename columns}
  (#_setResizeMode (#_horizontalHeader (tableWidget instance)) 0 (#_QHeaderView::Stretch))
  (#_hide (#_verticalHeader (tableWidget instance)))
  (#_setEditTriggers (tableWidget instance) (#_QAbstractItemView::NoEditTriggers))
  )

(defmethod timeline-mainform-ui-load-data ((instance timeline-mainform))
  (let ((item)
        (count)
        (row-index 0)
        (search-input (string-trim " " (#_text (lineEdit instance))))
        (rows))
    (setq rows (get-all-my-notes-from-db search-input))
    (#_clearContents (tableWidget instance))
    (setq count (length rows))
    (#_setRowCount (tableWidget instance) count)
    (with-slots (tableWidget) instance
      (dolist (item-row rows)
        (when item-row
          (setf item (#_new QTableWidgetItem (cadr item-row)))
          (#_setItem tableWidget row-index 0 item)
          (setf item (#_new QTableWidgetItem (write-to-string (car item-row))))
          (#_setItem tableWidget row-index 1 item)
          (incf row-index)
          )))))

(defmethod timeline-mainform-close-event ((instance timeline-mainform) close-event)
  (sqlite:disconnect *db*) ;;Disconnect
  (#_accept close-event))

(defmethod timeline-mainform-show-dialog-detail ((instance timeline-mainform))
  (#_exec (detailDialog instance)))

;;; #<QTableWidgetItem NULL> (was an issue)
(defmethod timeline-mainform-selected-item ((instance timeline-mainform))
  (let ((item (if (> (#_currentRow (tableWidget instance)) -1) (#_item (tableWidget instance) (#_currentRow (tableWidget instance)) 1) nil)))
    (when item
      (if (not *entity-note*)
          (setf *entity-note* (make-instance 'entity-note :ui-id (#_text item)))
          (setf (ui-id *entity-note*) (#_text item))
          )
      (load-from-db *entity-note*)
      ;;; load entity-data on dialog
      (#_setDate (date-obj (detailDialog instance)) (#_QDate::fromString (entity-date *entity-note*) "yyyy-MM-dd"))
      (#_setText (title-obj (detailDialog instance)) (title *entity-note*))
      (#_setText (content-obj (detailDialog instance)) (description *entity-note*))
      ;;; set read-only the date nil is false (not nil) is true
      (#_setEnabled (date-obj (detailDialog instance)) nil)
      (#_exec (detailDialog instance))
      )
    )
  )

(defmethod timeline-mainform-on-add ((instance timeline-mainform))
;;; ui-id (4 knowing if it's new)
  (if (not *entity-note*)
      (setf *entity-note* (make-instance 'entity-note :ui-id nil :title "" :description ""))
      (progn
        (setf (ui-id *entity-note*) nil)
        (setf (title *entity-note*) "")
        (setf (entity-date *entity-note*) nil)
        (setf (description *entity-note*) "")
        )
      )
  (#_setDate (date-obj (detailDialog instance)) (#_QDate::currentDate)) ;;; was set on timeline-main
  (#_setText (title-obj (detailDialog instance)) (title *entity-note*))
  (#_setText (content-obj (detailDialog instance)) (description *entity-note*))
  (#_setEnabled (date-obj (detailDialog instance)) (not nil))
  (#_exec (detailDialog instance))
  )

(defmethod timeline-mainform-on-delete ((instance timeline-mainform))
  (let (
        (mylist (#_selectedIndexes (tableWidget instance) ))
        (mycount)
        (my-row-index)
        (myitem)
        (mytitle)
        (mymessage)
        (my-yes-no-var)
        (result-yes (enum-value (#_QMessageBox::Yes)))
        )
    (when mylist
      (setq mycount (length mylist))
      (when (= (- mycount 1) 0)
        (setq my-row-index (#_row (car mylist)))
;;; get item
        (setq myitem (#_item (tableWidget instance) my-row-index 1))
        (when myitem
          (setq mytitle (concatenate 'string "delete record with ID=" (#_text myitem))
                mymessage (concatenate 'string "Sure, about deletion of record with Id=" (#_text myitem) "?"))
          (setq my-yes-no-var (#_QMessageBox::question instance mytitle mymessage (#_QMessageBox::Yes) (#_QMessageBox::No)))
          (when (= (enum-value my-yes-no-var)  result-yes)
            (if (not *entity-note*)
                (setf *entity-note* (make-instance 'entity-note :ui-id (#_text myitem)))
                (setf (ui-id *entity-note*) (#_text myitem))
                )
            (delete-from-db *entity-note*)
            ))))))

(defmethod timeline-mainform-on-ok ((instance timeline-mainform))
  (timeline-mainform-selected-item instance)
  )

(defmethod timeline-mainform-line-edit-text-changed  ((instance timeline-mainform))
  (let (
        (input (string-trim " " (#_text (lineEdit instance))))
        )
    (when (or (= (length input) 0) (> (length input) 0))
      (timeline-mainform-ui-load-data instance)
      )
    )
  )

(defun get-all-my-notes-from-db (search-input)
  (let ((sql-query "SELECT id, date(date) || ', ' || title FROM time_lines"))
    (if (> (length search-input) 0)
        (setq sql-query
              (concatenate 'string
                           sql-query
                           " where title like '%"
                           search-input
                           "%' or "
                           " description like '%"
                           search-input
                           "%' or "
                           " id like '%"
                           search-input
                           "%' "
                           )
              ))
    (setq sql-query (concatenate 'string
                                 sql-query
                                 " order by date desc"))
    (execute-to-list *db* sql-query)
    )
  )

(defun get-note-from-db (id-param)
  (execute-to-list *db* (concatenate 'string "SELECT date(date), title, description FROM time_lines where id=" id-param))
  )

;;; this frame is created dinamically
(defclass note-dialog ()
  ((date-obj :accessor date-obj)
   (title-obj :accessor title-obj)
   (content-obj :accessor content-obj)
   (parentFrame :accessor parentFrame)
   )
  (:metaclass qt-class)
  (:qt-superclass "QDialog")
  (:slots
   ("updateEntityObject()" note-dialog-update-entity-object))
  )


(defmethod initialize-instance :after ((window note-dialog) &key)
  (new window)
  (#_setWindowTitle window "Mis detalles")
  (let ((layout (#_new QFormLayout))
        (date-edit (#_new QDateEdit))
        (input (#_new QLineEdit  ""))
        (text-edit (#_new QTextEdit))
        (buttons (#_new QDialogButtonBox
                        (enum-or
                         (#_QDialogButtonBox::Ok)
                         (#_QDialogButtonBox::Cancel))
                        (#_Qt::Horizontal)))
        )
    (setf (date-obj window)  date-edit)
    (setf (title-obj window)  input)
    (setf (content-obj window)  text-edit)
    (#_setLayout window layout)
    (#_addWidget layout date-edit)
    (#_addWidget layout input)
    (#_addWidget layout text-edit)
    (#_addWidget layout buttons)
    (#_setDisplayFormat date-edit "yyyyMMdd")
    (qt:connect buttons "accepted()"
             window "updateEntityObject()")
    (qt:connect buttons "accepted()"
             window "accept()")
    (qt:connect buttons "rejected()"
             window "reject()"))
  )

(defmethod note-dialog-update-entity-object ((instance note-dialog))
  (setf (title *entity-note*) (#_text (title-obj instance)))
  (setf (entity-date *entity-note*) (#_toString (#_date (date-obj instance) ) "yyyy-MM-dd"))
  (setf (description *entity-note*) (#_toPlainText (content-obj instance)))
;;; save object to db
  (if (ui-id *entity-note*)
      (update-to-db *entity-note*)
      (insert-to-db *entity-note*)
      )
;;; update list ui with the changes (chk wxwidgets)
  (timeline-mainform-ui-load-data  (parentFrame instance))
  )

(defclass entity-note ()
  ((ui-id :accessor ui-id
          :initarg  :ui-id)
   (title :accessor title
          :initarg :title)
   (description :accessor description
                :initarg :description)
   (entity-date :accessor entity-date)
   ))

(defmethod load-from-db ((entity entity-note))
  (let (
        (rows (get-note-from-db (ui-id entity))))
    (dolist (item-row rows)
      (setf (title entity) (cadr item-row))
      (setf (entity-date entity) (car item-row))
      (setf (description entity) (cadr (cdr item-row)))
      )
    )
  )

(defmethod insert-to-db ((entity entity-note))
  (if (not (ui-id entity))
      (execute-non-query *db* (concatenate 'string "insert into time_lines (date, title, description, created_at) values (date('" (entity-date entity) "'), ?, ?, datetime('now', 'localtime') )") (title entity) (description entity))
      )
  )

(defmethod update-to-db ((entity entity-note))
  (if (ui-id entity)
      (execute-non-query *db* (concatenate 'string "UPDATE time_lines set title=?, description=?, updated_at=datetime('now', 'localtime') where id=" (ui-id entity)) (title entity) (description entity))
      )
  )

(defmethod delete-from-db ((entity entity-note))
  (if (ui-id entity)
      (execute-non-query *db* (concatenate 'string "delete from time_lines where id=" (ui-id entity)))
      )
  )

(defun timeline-main()
  (qt:ensure-smoke "qtuitools")
  (make-qapplication)
  (with-objects (
                 (mainform (make-instance 'timeline-mainform))
                 (dialog_detail (make-instance 'note-dialog))
                 )
    (setf (detailDialog mainform) dialog_detail)
    (setf (parentFrame dialog_detail) mainform)
    (#_show mainform)
    (#_exec *qapplication*)))

Mostrando el form o frame del designer-qt4

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>Form</class>
 <widget class="QWidget" name="Form">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>806</width>
    <height>475</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Form</string>
  </property>
  <layout class="QVBoxLayout" name="verticalLayout">
   <item>
    <layout class="QFormLayout" name="formLayout">
     <item row="0" column="0">
      <widget class="QLabel" name="label">
       <property name="text">
        <string>&amp;Search</string>
       </property>
      </widget>
     </item>
     <item row="0" column="1">
      <widget class="QLineEdit" name="lineEdit"/>
     </item>
    </layout>
   </item>
   <item>
    <layout class="QHBoxLayout" name="horizontalLayout"/>
   </item>
   <item>
    <widget class="QTableWidget" name="tableWidget">
     <property name="columnCount">
      <number>3</number>
     </property>
     <attribute name="horizontalHeaderDefaultSectionSize">
      <number>100</number>
     </attribute>
     <column>
      <property name="text">
       <string>uno</string>
      </property>
     </column>
     <column>
      <property name="text">
       <string>dos</string>
      </property>
     </column>
     <column>
      <property name="text">
       <string>tres</string>
      </property>
     </column>
    </widget>
   </item>
   <item>
    <layout class="QHBoxLayout" name="horizontalLayout_2">
     <item>
      <widget class="QPushButton" name="pushButton">
       <property name="text">
        <string>&amp;Add</string>
       </property>
      </widget>
     </item>
     <item>
      <widget class="QPushButton" name="pushButton_3">
       <property name="text">
        <string>&amp;Del</string>
       </property>
      </widget>
     </item>
     <item>
      <widget class="QPushButton" name="pushButton_4">
       <property name="text">
        <string>&amp;Ok</string>
       </property>
      </widget>
     </item>
     <item>
      <widget class="QPushButton" name="pushButton_2">
       <property name="text">
        <string>&amp;Cancel</string>
       </property>
      </widget>
     </item>
    </layout>
   </item>
  </layout>
 </widget>
 <resources/>
 <connections/>
</ui>

Mostrando el script de creación de la base de datos

echo "CREATE TABLE IF NOT EXISTS time_lines (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, title varchar(255), description text, date date, created_at datetime, updated_at datetime);" | sqlite3 wx_timelines.sqlite3

Como ejecutamos el timeline-commonQTApp desde el cli {requisito tener instalado el commonQT y el cl-sqlite}

sbcl --noinform --eval '(progn (ql:quickload :sqlite))' --load ~/dev/cl/timeline/timeline.lisp --eval '(progn (peapp-qt:timeline-main) (sb-ext:quit))'

Una vez terminado el desarrollo también podríamos compilarlo a código de maquina

sbcl --noinform --eval '(progn (ql:quickload :qt) (ql:quickload :sqlite))' --eval "(compile-file \"timeline.lisp\")" --eval '(progn  (sb-ext:quit))'

Si lo hemos compilado, debemos ejecutarlo de una manera diferente, para que el sbcl use el compilado en lugar del fuente

sbcl --noinform --eval '(progn  (ql:quickload :sqlite))' --load ~/dev/cl/timeline/timeline --eval '(progn (peapp-qt:timeline-main) (sb-ext:quit))'

Mostrando el timeline-commonQTApp

Imagen muy parecida a la que ya vimos mas arriba.

Detalle de un registro.

Antiguas Limitaciones del aplicativo

UPDATE: 20230908. releyendo este articulo 5 años después, nos dimos cuenta que no se grababan caracteres raros como el apostrofe, eso es debido a que el clsql-sqlite3 no soporta 'parameter binding', en plano castellano seria 'parámetros ligados o parámetros asociados'. Hemos cambiado la libreria clsql-sqlite3 por la libreria cl-sqlite , y ya se pueden grabar caracteres raros. Aprovechando que hemos estado viendo el QT, para portar el App OneHandMenu a QT y que de esa manera pueda ejecutarse en el Nokia N9, el único teléfono que tiene el sistema operativo Meego. Portaremos también el timeline a QT. Estar atentos a ese siguiente articulo.

Mensajes de Warning en la linea de comando al ejecutar el aplicativo

Al volver a ejecutar el timeline-commonQTApp directamente de la linea de comandos, después de instalar nuevamente el commonQT en la 'orange pi', nos percatamos de que aparecen varios mensajes de warning sobre los diferentes métodos, en el pasado cuando se ejecutaba desde SLIME estos mensajes no se notaban, debió ser porque evaluaba solo los métodos o funciones que cambiaban. Si alguien tiene idea de como resolver estos mensajes de Warning, nos lo hace saber.

Listado de Warnings:


https://disk.yandex.com/d/4lvry_HUBPlf_g

El dialogo de detalle se crea dinámicamente

Si observamos el código presentado con detenimiento, notaremos que existen 2 métodos que tienen el mismo nombre en 2 clases distintas 'timeline-mainform' y 'note-dialog' el método lleva el nombre de 'initialize-instance', en el caso de mainform usa un archivo con extension ui 'timeline_main_form_widget.ui' creado usando 'designer-qt4', sin embargo el 'note-dialog' crea todo el dialogo dinámicamente en el código, lo que es bastante interesante.

Ejecutandolo desde SLIME

Cuando se corre el timeline-commonQTApp desde el Slime, lo que mas demora es evaluar el buffer o evaluar el código que cambio, después ejecutarlo es prácticamente inmediato, y también aparecen los warnings mencionados con anterioridad.

Tip para instalar commonQT and cl-sqlite

En el 2023, cuando intentábamos ejecutar nuevamente este aplicativo desarrollado en 2018, el comando 'ql:quickload ya no era capaz de instalar el commonQt, así que tuvimos que clonarlo dentro de la carpeta '~/.quicklisp/local-projects' y lo mismo hubo que hacer para la libreria cl-sqlite

Nota acerca de shadowing

Qt es decir commonQT tiene un método 'connect' que nos sirve para conectar una señal y un slot y cl-sqlite también tiene un método 'connect' para conectarse a la base de datos sqlite, al definir el paquete 'defpackage' e indicar que este paquete usa qt y también usa sqlite nos dio un problema de 'name-conflict', así que cada vez que intentábamos ejecutar el timeline-commonQTApp nos aparecía el error y nos solicitaba información de como resolver el 'conflicto de nombre'. Para corregir dicho problema hubo que modificar el código para que cada llamada a connect se le anteceda el nombre del paquete para evitar ambigüedades, sin embargo aparte de ello hubo también que agregar la linea 'shadowing-import-from', en este caso estamos pidiendo que el método connect del paquete o librería sqlite no reemplace ni sobrescriba al método connect del paquete o librería qt, y con ello ya podemos ejecutar nuevamente el timeline-commonQTApp desde el Slime y también desde la linea de comandos sin que el sbcl nos pregunte como debería de resolverse dicho conflicto.

Conclusión

Existen pocos ejemplos de aplicativos en commonQt. Imaginamos que este sera uno de los pocos blogpost acerca del tema en el presente año.

Instalar commonQT y las librerías necesarias para que commonQt funcione es un poco trabajoso.

Instalar configurar y usar el Slime también es una tarea que requiere una explicación aparte, cuando no lo trabajas comúnmente cuesta un poco recordar como ejecutar el Slime. Después de cinco años hubo que revisar nuestras anotaciones previas.

Last change: 14.09.2023 14:22

blog comments powered by Disqus