Your techniques for MaxGUI

BlitzMax Forums/MaxGUI Module/Your techniques for MaxGUI

USNavyFish(Posted 2008) [#1]
I'm new to MaxGUI and would like some advice regarding data structuring of your MaxGUI programs. Below is a screenshot of a test program I've made.



When you click one of the buttons, the canvas adjusts to the appropriate color, and the sliders and textboxes adjust to represent the appropriate values. Likewise, when you modify a slider, the canvas changes color and the corresponding textbox modifies it's value - vice versa if you input a number to the textbox.

Here's the code when I click a button:

Function Button1_GA( Button:TGadget , GadgetList:TList=Null )
	DebugLog "Button Button1 was pressed"
	
CCR=255
CCG=0
CCB=0

SetSliderValue(SLR,255)
SetSliderValue(SLG,0)
SetSliderValue(SLB,0)

SetGadgetText(txtR,String(255))
SetGadgetText(txtG,String(0))
SetGadgetText(txtB,String(0))

RedrawGadget(MainCanvas)


End Function



(CCR,CCG,and CCB are all global values which the MainCanvas reads upon redraw)


What a mess! I have to manually modify every other gadget whenever I click one of them!


So what I'm looking for is a more efficient solution. How do you guys do it? I would like to 'link' the gadgets together, likely using events, so that whenever a SINGLE VALUE is changed, for example, CCR is modified, then ALL gadgets which depend upon or modify CCR will be automatically notified and updated as appropriate.


CS_TBL(Posted 2008) [#2]
Well euhm.. as you can see you are 3 times changing values, which in this case are the same, being 255,0,0

How about replacing those 9 lines with 1? :P
Function SetPalettecolor(r:int,g:int,b:int)
CCR=r
CCG=g
CCB=b

SetSliderValue(SLR,r)
SetSliderValue(SLG,g)
SetSliderValue(SLB,b)

SetGadgetText(txtR,String(r))
SetGadgetText(txtG,String(g))
SetGadgetText(txtB,String(b))

End Function


Untested, but I guess it works. Now, from every gadget handler you call this function for every rgb color you want.

Naturally, since you didn't paste the rest of your code, I can only assume that all the variables and objects are global or otherwise within reach of that function.

If you're asking for a more overall large-scale structure, we need to see more.


CS_TBL(Posted 2008) [#3]
btw, this is separating GUI from implementation.

You did the implementation in the GUI handler and so you're a hardwiring your logic. Think in terms of machines. What does the machine do?

1 - In your case, a 'palette' or 'color' is the machine.
2 - The machine has a method to change the color, and that's the only thing this method does.
3 - You let a GUI object call this method
4 - or yet better: you let a GUI object send out an event and you let the color machine wait for that event.


USNavyFish(Posted 2008) [#4]
Both things you posted make alot of sense.

I agree that separating GUI from implementation is pretty vital.

What is the advantage of item #4 (sending an event) over item #3? Flexibility? Could you somehow embed the new parameters in the event?


These are very good answers. I am looking for one missing link, and that is a way for GUI elements to 'read' global values once they change and adjust their displayed setting to match those values. This is easy if you have each gadget redraw every cycle, but of course that is wasteful of cpu resources. I suppose an event hook could call the appropriate gadget redraws, and the gadgets would read global values. This is still not a 'universal' technique, because I would still need to link the appropriate GUI elements together in the source code.


A better solution might be to have some means of "watching" a variable. Whenever that variable changed, it would notify all gui elements which are connected to that variable. Of course, you'd have to build a list of which elements are associated with which variable, but that would be easy to perform just once during the initial gadget creation.


Hmm, I think I could do this in a custom type.. The idea stream is flowing!


CS_TBL(Posted 2008) [#5]
Advantage of #4 is that you're not hardwiring objects together. This means that your objects don't need to be global (=potentially messy), and one object doesn't need to contain a reference to another object (=hardwiring).

btw, if we split your RGB changer into 3 components (r,g and b) then each 'thing' you need is a composite of resetbutton, slider and editbox. Why not make that composite into a new object reacting on unique new events?

You eventually create 3 such components, or you don't bother and create 1 big RGB component with 3 resets, 3 sliders and 3 editboxes. Either way: such a big complete 'rgb machine' has these methods:

- Changevalue(component,value) ' component: 0:r, 1:g, 2:b

It sends out these events:

- Colorchanged (an event sent whenever you change the color, other objects elsewhere will hear this event and update according to the new rgb settings, you could add Self to the extra field of the tevent object to actually send the rgb values to the listening objects via the event system, which is not hardwiring, yet the listener does have access to the source!)
- Colorchanging (an event sent when you are moving sliders but haven't released these sliders, afaik this is not possible with native sliders, but it is possible with canvas-based new sliders. This may be useful, you could change a palette overview with this event and update a picture (which is more heavy to update) when you send out a Colorchanged event)
- Colorstartchange (an event sent out when you first touch the sliders, this may not really be needed, but you could signal a heavy section (like an image) to not listen to Colorchanging events until a Colorchanged event has been sent, to prevent timeconsuming updates while changing colors)

It can listen to these events:

- Changecolor (for instance, you colorpick somewhere in an image, and the resulting RGB values should be sent back to your RGB sliders, the event.extra field should then contain the object that handled the colorpicking OR simply contain an int containing the RGB value, which could as well be stored in the .x, .y, .data fields for all I care)



The nice bit is that you can have as many RGB sliders as you want, you only need to create them and place them, and they will work instantly, without connecting anything. Because it sends out events, it'll work right out of the box.

Also nice: you can take them away and your program won't be buggy, it just lacks features, but it won't give errors. It will send out events, but when no-one listens, nothing happens. If however you hardwire things, you will get errors!


USNavyFish(Posted 2008) [#6]
Excellent advice, CS_TBL. I am beginning to view the GUI from a much more 'separated' OOP perspective, which is clearly advantageous.

This piques my interest over events. There is more to them than I am immediately familiar with. For example, I have not yet dealt with this "extra" field of the tevent object. I have only played with System Timer events, which send their own custom messages. From your description, however, it looks like I can create a tevent object from within a method, set its fields to various values, and then emit that event to Blitzmax's event distribution system.

When I place an object in the 'extra' field of the tevent object, what happens? Does the event send a message specifically to that instance/type? If either of these are true, where in the object's code is that message received (i.e. what function/method is called)? Or am I wrong assuming messages can be sent directly to objects?

If you wouldn't mind describing a little more about how to use custom events (or pointing me to a good in-depth tutorial), I'd be very grateful! You've already helped alot.

NAVY


CS_TBL(Posted 2008) [#7]
will see, meanwhile:
http://www.blitzmax.com/Community/posts.php?topic=68626


jsp(Posted 2008) [#8]
CS_TBL explained it very very good.

If you need more examples, have a look into the SmartButton source code. They are completely event based and emitting also their own events and using the extra object.


What is the advantage of item #4 (sending an event) over item #3? Flexibility? Could you somehow embed the new parameters in the event?


I use #4 for my own gui constructs i wanna use ever and ever again and #3 for updating just normal gadget stuff.


USNavyFish(Posted 2008) [#9]
I agree, CS_TBL's explanation and SuperSpinner provided some nice assistance.

I've been off on a different tangent lately, but am now coming back to the GUI part of my latest project.

I may just post a few more questions to this thread as they pop up, but again thank you everyone.


USNavyFish(Posted 2008) [#10]
2 questions for CS_TBL

in the following event hooker:

Function eventhook:Object(id:Int,data:Object,context:Object)
		If TSmartspinner(context) TSmartspinner(context).ev TEvent(data);Return data	
	EndFunction



I understand that you are casting the context object as a TSmartSpinner, and then passing data cast as a TEvent to its 'ev' method. But what does the condition if TSmartspinner(context) test?

It seems like it simply checks the existence of the context object.. but of course, there would be no need to cast it as a TSmartSpinner object if that were the case, so apparently there's something else going on here i'm not aware of. Would you mind filling me in?



Second question, in your update() method, you emit a new event with id = EVENT_SMARTSPINNERUPDATE. In the documentation you explain that this is to be captured by external hook functions. Could you give me an example of why you would want this to be hooked in a program? I.e. what would a hooker do upon receiving the above event?

I think that will help me understand the usefulness a bit more. Thanks CS!


USNavyFish(Posted 2008) [#11]
And in the meantime, I have a general question for anybody..


I have created a Type which contains within it my interface. All gadgets within that type are globals.

If I 'extend' that Type with a sub-type, will the sub-type's own gadgets be able to speak to (i.e. be children of) the Base Type's gadgets? If so, I can create a 'base' interface for use in many projects, and then simply 'extend' it with a custom interface type that is specific for each particular program's functions.

It's probably not the 'best' OOP solution in terms of separability, but I think it suits my needs well enough.


It's past midnight here so I'm off to bed otherwise I'd answer my own question tonight. I will test it out after work tomorrow afternoon.. if no one's responded. Just was looking for a quick answer if anyone had ever tried something like that before..



EDIT: I figured I'd include my current code, just for kicks. Please note this is the 'base' interface class I was describing. Eventually it will contain within it all "non-specific" framework functions, such as handling window open/close, menu function (i.e. opening up the options window which will allow you to change the resolution of the canvas), minimize/maximize events, window moving, etc.

After that's built, I'll extend an 'Application-Specific' Sub-Type upon the base class, which will add all the buttons, tabs, sliders, etc that pertain to my specific program -- not to mention the canvas operations (likely an OpenGL context). This is why I'll need the sub-type's gadgets to be able to child-group with the base class' window.


Note how the canvas moves around 'locked' to the 'interface window'. That was Kev's idea (thanks again!) to use an event hook upon the EVENT_WindowMove, since MDI isn't officially supported.


enough english, more blitzmax:




CS_TBL(Posted 2008) [#12]
I understand that you are casting the context object as a TSmartSpinner, and then passing data cast as a TEvent to its 'ev' method. But what does the condition if TSmartspinner(context) test?


I dunno, I always c/p this thing from source to source, only changing the names. I'd say: it works, don't bother with it, it'll only give you headaches trying to understand it, and you won't really gain much when you do. :P

Second question, in your update() method, you emit a new event with id = EVENT_SMARTSPINNERUPDATE. In the documentation you explain that this is to be captured by external hook functions. Could you give me an example of why you would want this to be hooked in a program? I.e. what would a hooker do upon receiving the above event?


I want the update (e.g. the drawing of the spinner) external and hooked, which has these advantages:
- they're not hardwired, meaning that all I need to do is create a function and hook it, I never need to touch the original spinner engine again.
- by having it external, I won't get a spinner engine with a 3000 lines Select-Case construction, containing all the possible spinners. (This was actually the main reason to investigate external hooked functions some years ago when I was working on a texture generator, wanting to avoid a core engine of 10000 lines just because it contained all the texture effects)


jsp(Posted 2008) [#13]
I understand that you are casting the context object as a TSmartSpinner, and then passing data cast as a TEvent to its 'ev' method. But what does the condition if TSmartspinner(context) test?






Method New()
AddHook EmitEventHook,eventhook,Self
End Method



The New method of the spinner takes the spinner itself (Self) as Object to the Hook and will be present there as Context.



Function eventhook:Object(id:Int,data:Object,context:Object)
If TSmartspinner(context) TSmartspinner(context).ev TEvent(data);Return data
EndFunction


EDIT to be more precise:
During processing of an event every hooked function will be checked.
When the event comes to the Spinner Hook function the instance we added via 'Self' is present in the Context. When we cast the context - stored as object - “Tsmartspinner(context)” to our original TSmartspinner, we are able to access the TSmartspinner methods (we call the .ev method of that type) and check if the current event we are processing were from this 'Self' instance.



Method ev(event:TEvent)

If event.source=canvas

If event.id=EVENT_GADGETPAINT update



When the event enters the ev method the first thing is to check which of the spinners this events belongs to. This is done here with “If event.source=canvas”. The canvas was set before in the type
Field canvas:tgadget

So, every spinner checks if his own canvas was producing the event. If it's matches then it processes the event....

Hope this helps


USNavyFish(Posted 2008) [#14]
Thank you! I understand now.

You all have been extremely helpful, I hope this thread helps others too!

-Navy


USNavyFish(Posted 2008) [#15]
Ironically, I'm now using a similar procedure, but in the opposite manner.

When one of my custom type emits its own custom events, I use this technique to prevent the emitting type from intercepting/reading its own events. I suppose this will save a few CPU cycles, as the emitting type's "OnEvent" manager has around 50 select-case conditionals. When it emits a custom event, there is no need for it to 'check' the emitted event, as the emission is always intended for another object type. Thus, the context-checking procedure above prevents the wasted conditional checks.