Search code examples
common-lispallegro-cl

In Allegro Common Lisp Form application on Windows is there global form and button variables?


Allegro Common Lisp forms are very much like Delphi forms. But delphi forms at least allow you to access global variables such as Form1, Button1, Button2, etc.

In Allegro common lisp the only way I can figure out how to access buttons properties and form properties is to use find-sibling and set up a local variable with LET, or set up my own global variable. Is there already a global variable to access widgets like button1, form1, etc in common lisp that you can access for convenience...

For example if I want to access button1 on form1 in Allegro CL from clicking another button 2, I would go:

(let ((but1 (find-sibling :button1 widget)))  
  (setf (title but1) "hello world" )) 

Using find-sibling is tedious and seems to be a waste of time compared to if there was just a global variable to access like in delphi:

form1.color := clBlue;
button1.caption := 'Hello world';
button2.caption := 'I am button2';

How do I setf button1 title (same as caption in delphi) in allegro common lisp without using find sibling.... I can use the widget function parameter (like delphi Sender as TButton) if I am talking to the object inside a function, but have to use find-sibling for other components. It just seems like allegro common lisp is forcing people to write find-sibling code instead of just giving you a global variable like button1 and button2 and form1.

Edit: in delphi form1 is global, but button1 and button2 are only part of the global form class, they are not global themselves, but they act like globals since you can do

form1.button1
form1.button2

from other units

(or self.button1 from the current unit1, but delphi does not demand you say SELF all the time, for keyboard typing convenience).

EDIT: Nope, the product "allegro common lisp" simply can't handle basic programming tasks because it is lisp, not a practical language.


Solution

  • It is a waste of time, but not much for most cases. I'll answer first and then provide some experience afterthought on this.

    This may be enough for most cases:

    (defmacro with-components ((&rest names) dialog &body body)
      (assert (every #'symbolp names))
      (let ((dialog-sym (gensym (symbol-name '#:dialog))))
        `(let ((,dialog-sym ,dialog))
           (let (,@(mapcar #'(lambda (name)
                               `(,name
                                 (find-component
                                  ,(intern (symbol-name name) :keyword)
                                  ,dialog-sym)))
                           names))
             ,@body))))
    
    (defun do-something (my-dialog)
      (with-components (my-progress-bar) my-dialog
        ;; ...
        ))
    

    One alternative is to define slots, thus a specific class, for your window.

    This is as close to Delphi or VB as you will get, because those use object fields, not global variables, for controls. It's just a matter of syntax and scope: whereas in some languages you can refer to an instance field inside a method, in Common Lisp you either use accessor functions/with-accessors or slot-value/with-slots.

    (defclass my-dialog (dialog)
      ((my-progress-bar :reader my-dialog-my-progress-bar)))
    
    (defmethod initialize-instance :after ((dialog my-dialog) &rest initargs)
      (declare (ignore initargs))
      (with-slots (my-progress-bar) dialog
        (setf my-progress-bar (find-component :my-progress-bar dialog))))
    
    (defun my-dialog ()
      (find-or-make-application-window :my-dialog 'make-my-dialog))
    
    (defun make-my-dialog (&key owner #| ...|#)
      (make-window :my-dialog
        :owner (or owner (screen *system*))
        :class 'my-dialog
        :dialog-items (make-my-dialog-widgets)
        ;; ...
        ))
    
    (defun make-my-dialog-widgets ()
      (list
       (make-instance 'progress-indicator
         :name :my-progress-bar
         :range '(0 100)
         :value 0
         ;; ...
         )
       ;; ...
       ))
    

    This can be further simplified with a macro where you define the name of the dialog items and their initargs, and it should generate the class with a slot per dialog item and the initialize-instance :after method, counting on the maker functions generated by the IDE.

    (defmacro defdialog (name (&rest supers) (&rest slots) &rest options)
      (let ((static-dialog-item-descs (find :static-dialog-items options
                                            :key #'first))
            (dialog-sym (gensym (symbol-name '#:dialog)))
            (initargs-sym (gensym (symbol-name '#:initargs)))
            (owner-sym (gensym (symbol-name '#:owner))))
        `(progn
    
           (defclass ,name (,@supers dialog)
             (,@slots
              ;; TODO: intern reader accessors
              ,@(mapcar #'(lambda (static-dialog-item-desc)
                            `(,(first static-dialog-item-desc)
                              :reader ,(intern (format nil "~a-~a"
                                                       name
                                                       (first static-dialog-item-desc)))))
                        (rest static-dialog-item-descs)))
             ,@(remove static-dialog-item-descs options))
    
           (defmethod initialize-instance :after ((,dialog-sym ,name) &rest ,initargs-sym)
             (declare (ignore ,initargs-sym))
             (with-slots (,@(mapcar #'first (rest static-dialog-item-descs))) ,dialog-sym
               ,@(mapcar #'(lambda (static-dialog-item-desc)
                             `(setf ,(first static-dialog-item-desc)
                                    (find-component
                                     ,(intern (symbol-name (first static-dialog-item-desc))
                                              :keyword)
                                     ,dialog-sym)))
                         (rest static-dialog-item-descs))))
    
           ;; Optional
           (defun ,name ()
             (find-or-make-application-window ,(intern (symbol-name name) :keyword)
                                              'make-my-dialog))
    
           (defun ,(intern (format nil "~a-~a" '#:make name))
               (&key ((:owner ,owner-sym)) #| ... |#)
             (make-window ,(intern (symbol-name name) :keyword)
               :owner (or ,owner-sym (screen *system*))
               :class ',name
               :dialog-items (,(intern (format nil "~a-~a-~a" '#:make name '#:widgets)))
               ;; ...
               ))
    
           (defun ,(intern (format nil "~a-~a-~a" '#:make name '#:widgets)) ()
             (list
              ,@(mapcar #'(lambda (static-dialog-item-desc)
                            `(make-instance ,(second static-dialog-item-desc)
                               :name ,(intern (symbol-name (first static-dialog-item-desc))
                                              :keyword)
                               ,@(rest (rest static-dialog-item-desc))))
                        (rest static-dialog-item-descs)))))))
    
    (defdialog my-dialog ()
      ()
      (:static-dialog-items
       (my-progress-bar #| Optional |# 'progress-indicator
         :range '(0 100)
         :value 0               
         ;; ...
         )))
    

    There are many options here.

    For instance, you may not want to automatically define an initialize-instance :after method, because you might want to define one yourself with business initialization logic, so you can instead initialize the slots in the dialog maker function. But then, you'll be fighting the IDE generated code (you can always use it for prototyping and then adapt your code), the reason why I denoted some code as optional.

    Or you could extend the macro to take initializing code as an argument (to include in the generated initialize-instance), or a separate macro to be used inside or instead of initialize-instance :after, or both, where the former would use the latter.


    I can tell you that when there are many UI updates, this minor, but repeated waste of time becomes relevant. And by many, I mean at least a few dozens of calls per second during a few dozen seconds or minutes. Most dialog windows shouldn't behave like this, as they'll just query data from the user or act like tool windows with action buttons.

    However, let's assume you fell into such a case, e.g. a progress dialog.

    Using accessors or slots instead of find will improve performance quite a bit, as you can see for yourself using Allegro's profiler, but that's just the top-most hot spot.

    It might become necessary to know if you really need a UI update in such circumstances, so keep some lightweight bookkeeping to know if you really need to touch the dialog or its items. This is actually very easy, and you might save more doing this than optimizing dialog item access. Good candidate data types are counters and timestamps.

    Yet another technique is to delay updates by determined intervals, perhaps with a timer that updates the UI batching up previous update requests (e.g. queue the update, start the timer if not started yet, make the timer be a one-off so it won't run when not needed, make the timer function reduce queued updates before actually updating). If you're expecting many updates per time unit, this might be the greatest optimization. However, it's also the most specific and laborious one and quite error prone if things fall out of simplicity.

    The gain is, if you implement that queue, you may earn inter-thread communication, e.g. registering UI updates on business model property change/state change/progress events, which might happen in non-UI background worker threads.

    PS: With this, I'm not saying you should implement only one of these approaches, I'm explaining what improvement over effort you get, in case you can't spend much time around this.


    PS: Allegro already has some support for cross-thread UI operation queueing through post-funcall-in-cg-process, including cumulative operations with the :delete-types argument and idempontent operations with the :unless-types argument.

    The catch is that this queue is processed only in event-loop which is typically used as the top-level event loop (versus a modal or menu event loop, or message processing that may happen in other functions). In non-event-loop message processing, the operations are not dequeued and not processed.