Меnu:


The application that today we will port to commonQt is a note management application, the application allows us to create notes and then easily search any term or keyword within the set of notes created. This application uses sqlite as the backend database.

Why commonQt and not just Qt?.

Risc is the future. It is a phrase from the movie Hackers of 1995. Well, several years ago we have ARM that is a sprout from Risc. We started in 2007 with the Nokia N800 tablet, three years ago we moved to the Nokia N900 phone and a couple of years ago (It was 2018) we have available an always-on machine that is the orange pi. One of the applications that we have not yet been able to migrate from x86 to Arm is the dbdesigner-fork. It is for modeling databases It have been developed in Kylix (Delphi for GNU/Linux), it has been many years since the Kylix have been abandon-ware. However the dbdesigner application is still working, therefore we continue using it. There have been attempts to migrate it to Lazarus but without success until today. And since our favorite text editor is Emacs, we thought it would be a good opportunity for making a small application in commonQt with the goal of coding an alternative to dbdesigner in commonQt in the near future . Due to the size of the dbdesigner , we expect commonQt help us accelerate the development. We'll see how it goes.

What do we will do?

We are going to port the app made in wxwidgets to commonQt . We will use the Qt designer in ArchLinux to design the user interface.

Previously we have written about how to make the application in 2 articles:

  1. missing wxwidgets sample.
  2. Missing wxwidgets example part 2.

Showing the App coded in wxwidgets

Showing the common-lisp code

(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/olla/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*)))

Showing the UI design file on 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>

Showing the database creation script

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

How to run the timeline-commonQTApp from the cli {requirement having installed commonQT and cl-sqlite}

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

How to compile the common lisp script

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

How to run the compiled binary {the one with the extension fasl}

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

Showing the timeline-commonQtApp

This one is similar to the one we have shown above.

This image is for a record detail

Old limitations of the timeline-commonQTApp

UPDATE: 20230908. Rereading this article 5 years later, we realized that rare characters such as the apostrophe were not saved to the database, this limitation was because clsql-sqlite3 does not support 'parameter binding'. We have replaced the clsql-sqlite3 library by the cl-sqlite library , and rare characters can now be saved to the database. Taking advantage of the fact that we have been seeing the QT, to port the OneHandMenu App to QT so that it can run on the Nokia N9, the only phone that has the Meego operating system . We will also port the timeline to QT. Stay tuned for that next article.

Warning messages on the command line when running the timeline-commonQTApp.

When re-running the timeline-commonQTApp directly from the command line, after installing commonQT again on the 'orange pi', we noticed that several warning messages appear about the different methods, in the past when it was run from SLIME These messages were not noticed, it must have been because it evaluated only the methods or functions that changed. If anyone has an idea how to resolve these Warning messages, please let us know.

Warning List:


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

The detail dialog is created dynamically

If we look at the code presented carefully, we will notice that there are 2 methods that have the same name in 2 different classes 'timeline-mainform' and 'note-dialog', the method is named 'initialize-instance', in the case of mainform uses a file with ui extension 'timeline_main_form_widget.ui' created using 'designer-qt4', however 'note-dialog' creates all the dialog dynamically in code, which is quite interesting.

Running it from SLIME

When you run the timeline-commonQTApp from the Slime, what takes the longest is evaluating the buffer or evaluating the code that you changed, then executing it is practically immediate, and the warnings mentioned above also appear.

Tip for installing commonQT and cl-sqlite

In 2023, when we tried to run this application developed in 2018 again, the 'ql:quickload' command was no longer capable of installing commonQt, so we had to clone it into the '~/.quicklisp/local-projects' folder and The same had to be done for the cl-sqlite library

Note about shadowing

Qt, that is, commonQT, has a 'connect' method that helps us connect a signal and a slot and cl-sqlite also has a 'connect' method to connect to the sqlite database, by defining the 'defpackage' package and indicating that This package uses qt and also uses sqlite, it gave us a 'name-conflict' problem, so every time we tried to run the timeline-commonQTApp we got the error and asked us for information on how to resolve the 'name conflict'. To correct this problem, the code had to be modified so that each call to connect was preceded by the name of the package to avoid ambiguities. However, apart from that, the 'shadowing-import-from' line had to be added,

Conclusion

There are few examples of applications in commonQt . We imagine that this will be one of the few blog posts about the subject this year.

Installing commonQT and the necessary libraries for commonQt to work is a bit of work.

Installing, configuring and using the Slime is also a task that requires a separate explanation, when you don't work with it it is usually a bit difficult to remember how to run the Slime. After five years we had to review our previous notes.

Last change: 14.09.2023 17:04

blog comments powered by Disqus