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.