-2

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.

Another Prog
  • 841
  • 13
  • 19
  • A 'global' variable? Really? Does that make sense? – Rainer Joswig Feb 10 '15 at 15:32
  • Rainer, global variables are not evil when used properly. In delphi global variables are rarely used, but when they are used properly they are extremely useful. Form1 in delphi is global because all other units such as form2 (unit2) need to have access to form1. The idea that global variables are always evil is a myth. They are useful in some situations, they are not always evil. To answer your question, yes it makes sense. Even the allegro common lisp documentation sets up a **global** variable for convenience to access a component. I will try to find it in their docs or examples – Another Prog Feb 10 '15 at 18:36
  • @RainerJoswig, for a top-level window, maybe. For a widget, no. Most probably, the shown Delphi code is inside a form's method, `button1` and `button2` are probably fields and not global variables, but it seems to be using a global variable called `form1`. If this is a `Form1` (the class) method, it's definitely not good practice. Instead, it should use `Self` or nothing at all, where the lexical context includes the fields. The `form1` global may be useful outside the class's methods, if the intent is to have at most one such window, but I'd say this is mostly useful for the main window. – acelent Feb 10 '15 at 18:42
  • Generally I would see this as a design mistake in Lisp. Finding the sibling makes more sense to me. – Rainer Joswig Feb 10 '15 at 18:42
  • find-sibling is just a global function available to all code that makes all components global if you want, so it is just the emperor in new clothes... – Another Prog Feb 10 '15 at 18:52
  • This is mostly a Lisp question, as opposed to a Common Graphics question. It seems the op wants to know why in Lisp you must use accessors, while in e.g. Delphi or VB you can access fields directly in methods. The global variable question is moot, it's actually just confusion, only `Form1` is a global variable, and you can do the same in Lisp. In Common Graphics, there's no automatic slot code generation for each widget, so I guess this should be the discussed issue, and the one I answered for. – acelent Feb 11 '15 at 17:03
  • Form1.button1 is not a method in delphi - the button is a variable held inside the class form1 ( a class can contain sub-variables inside the other main variable). What I wanted to do in allegro lisp was avoid calling find-sibling and just go (SETF ***button1*** title " hello world") since this is very intuitive and avoids useless boilerplate code to find-sibling each time you want to talk to a component. Your solutions are definitely something I will consider, but I wonder why franz hasn't implemented an elegant solution already, or if we have missed something obvious – Another Prog Feb 11 '15 at 21:48
  • I don't think that it is a moot point that form1 is a global variable in delphi. In the allegro common lisp documentation and examples, it says they set up a widget as a global variable for convenience ... I will try to find the docs where they setup a global variable to access a widget throughout the program. My point is that maybe all widgets should be somehow globally accessible without calling find-sibling each time, because you often want to modify your widgets at run time, that's the whole point of a GUI program, to modify and talk to your widgets and change those widgets at run time! – Another Prog Feb 11 '15 at 22:03
  • Here it is from the documentation.. global variable for convenience: "We'll reuse this single chart-widget throughout the tutorial, and so we'll bind the global variable *chart-widget* to the tutorial widget to make it easy to modify it with top-level forms. And since we'll be changing the state of this single widget through the tutorial, it's important to evaluate the tutorial code sequentially, since some steps undo a state that was created in the previous step." They setup a global variable to access the widget often. IMO widgets SHOULD be accessible without setting all this up yourself – Another Prog Feb 11 '15 at 22:24
  • In that case, I advise you to stick with Delphi or similar, because hating a syntax is too awful to put up with every day, specially if character count is your main concern. I don't know if you saw the comments I added below explaining the advantage Lisp has regarding syntax, but given your reasoning, I guess you don't care. The global variable is used in the tutorial so you can use it in the REPL, another powerful thing (unlike statically compiled languages, it allows you to inspect, change, add and remove variables, classes, functions and whatnot in the running environment). – acelent Feb 12 '15 at 11:47
  • I suggest you learn a bit of Lisp before evaluating where a Lisp GUI framework shines or smells without knowing what is normal in the underlying language. You won't find any Lisp GUI framework that eases this for you. LispWorks' CAPI, which already has [a similar macro](http://www.lispworks.com/documentation/lw61/CAPUG-W/html/capiuser-w-134.htm) to `defdialog` (see answer), doesn't save your code (e.g. events) from using accessors. Note that even Delphi and VB.NET define a global variable for `Form1`, only VB6/VBA did that behind your back. – acelent Feb 12 '15 at 11:52
  • By choosing Lisp, you'll have to get used to this: `(setf (title (find-component :button1 (form1))) "hello world")`, or `(setf (title button1) "hello world")` within `with-components` (see answer), `with-accessors` or `with-slots`. That's why I recommend you don't choose Lisp if this bothers you that much. – acelent Feb 12 '15 at 11:52

1 Answers1

2

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.

acelent
  • 7,965
  • 21
  • 39
  • I appreciate the code you have offered. I think allegro common lisp should ship the product with the functionality built in instead of us having to add our own code to do something as simple as access widgets properties at run time. In delphi, form1 is in fact a global variable in the unit and this makes it easy to access components on form1 – Another Prog Feb 10 '15 at 10:43
  • In Delphi, only `Form1` is a global variable, its controls are accessible through the form's instance fields. You can mimic that by using `(form1)`, since it's usually implemented through `find-or-make-application-window`. Alternatively, you can optimize it e.g. `(defvar *form1* (form1))` and then use `*form1*`. – acelent Feb 10 '15 at 13:13
  • You may argue that this should be base functionality, so if you're a customer, you should request it to Franz Inc., discussing it here isn't going anywhere. On the other hand, Allegro does come with helpful examples, so it's not like you're in the mud about accessing widgets, I quickly found `find-component`, `find-sibling` and `find-named-window` in the examples/cg directory. Using `find-component` is not that bad, and the first macro eases it. – acelent Feb 10 '15 at 13:14
  • The reason I have not yet suggested it to Franz is because i want to discuss the idea first to see if there are any disadvantages or existing ways to do what I am requesting without writing silly code all the time. In delphi, it is just easy and there for you as a global so I wanted to see what franz offered and their reasoning. If the reason is that globals are evil, then I disagree, since globals are sometimes not evil and are the only solution. If the reason has to do with the way functional programs work compared to imperative ones and there is no simple way to do it.... well.. we will see – Another Prog Feb 10 '15 at 18:43
  • To what end are you comparing Delphi and Allegro CL, if it's not much to ask? – acelent Feb 11 '15 at 09:56
  • I am comparing the two because one programming tool seems to have an elegant solution to the problem and the other (Allegro CL) seems to have a solution that is not very elegant... but it may just be that Lisp programming language doesn't allow the same thing as imperative language like delphi, and functional programming requires writing more functions to do things whereas imperative structured coding requires less typing and less code to do something as SIMPLE as setting the text of a button (caption) to hello world. A hello world program should not be complicated IMO. Complex = bad. – Another Prog Feb 11 '15 at 21:57
  • A long time ago, there were a few object systems , such as [Flavors](https://common-lisp.net/project/bknr/static/lmman/flavor.xml#Simple%20Use%20of%20Flavors-section), that provided the ability to refer to slots like variables. This fell out of use when generic functions were preferred to messages, and perhaps mostly because invoking an accessor has historically been the way to access a struct slot too. So, you have to take this into perspective, it has nothing to do with imperative vs functional programming per se. – acelent Feb 11 '15 at 22:40
  • Also, if you feel something is missing in Delphi's syntax, you won't be able to extend it as easily as with Lisp's macros, by far. In fact, most languages don't allow you to extend syntax in any way, and some allow you to have textual macros (i.e. they can't really manipulate code, they're mostly find-and-replace), but as far as I know, Lisps are still very unique general purpose programming languages in this respect. – acelent Feb 11 '15 at 22:45
  • all I can say is, what an absolute joke. People will be running back to Delphi and C# after they try this joke of a product. Are you serious? just to.. change the button color or text, you have to be a masochist and jump through thousands of tarded lisp hoops. even power basic is better than this. Thanks bye! – Another Prog Mar 08 '16 at 04:57
  • I'm glad you finally made a choice, but I'm sad about the shallow way you did it. It seems you did not try it out any further than changing the text and color of a button, so at least I'm glad you made the right choice if this is the kind of thing you'll be developing. – acelent Mar 08 '16 at 09:49