Tek-Tips is the largest IT community on the Internet today!

Members share and learn making Tek-Tips Forums the best source of peer-reviewed technical information on the Internet!

  • Congratulations TouchToneTommy on being selected by the Tek-Tips community for having the most helpful posts in the forums last week. Way to Go!

Macro's 1

Status
Not open for further replies.

D-Ward

IS-IT--Management
Sep 6, 2022
30
GB
Happy New Year to all

I am creating different sets of dynamic shape objects on an image in a container, firstly a load of squares that represent physical objects on a floor plan drawing. I then create a load of line objects (which are really shapes, that messed with me for a bit) that link some of the squares to show that they are in a group. When the line is created the TAG property is set to the 'sequential' part of the shape name when it was created.

I can get all this working but it is really ugly code and am sure there is a more elegant, easier way to manage the code. What I want to do is something like;

FOR ox = thisform.CONTAINER.ConrtrolCount TO 1 STEP -1
IF LEFT(thisform.CONTAINER.Objects(ox).Name, 7) = "LINE"
_Macro = "WITH thisform.Container.SHAPE" + thisform.CONTAINER.Objects(ox).Tag
&Macro
thisform.CONTAINER.Objects(ox).Left = .Left + (.Width / 2)
thisform.CONTAINER.Objects(ox).Top = .Top + (.Height / 2)
ENDIF
NEXT

It doesn't like the &Macro command as throw a 'WITH/ENDWITH mismatch' error, so guess you can not use a Macro to instigate a WITH, FOR, SCAN or other such command.

I have done similar projects in the past in other languages, but you were able to create object as arrays thisform.SHAPE(1) so it was easier to reference them using an integer for the array name, but as I have to build the sequential identifier into the object name it make things a little different.

I'm not sure if the above makes sense, but would appreciate any ideas how to best handle this.

Regards,

Darren





 
Darren,
What is the value of &Macro at the time it is called?
The value of Macro is "WITH thisform.Container.SHape<contents of .Tag>"

When that gets executed there is no ENDWITH, and whatever the value of that .Tag (of that object) is, you're concatenating it directly to the end of the text in " ", so that is going to be a bad reference as well, you need to at least put a space after "SHAPE" in your test string.
THis is why you get a "WITH/ENDWITH" mismatch, because no ENDWITH is every created using your macro substitution. Also I highly recommend using name substitution rather than macro sub as well, as in (Macro) instead of &Macro or better EVALUATE(Macro).

So unless the value of <object>.Tag is "Endwith" you're "With" never has a matched end.
If in doubt of the value of Macro at the time, before you call &Macro add the line:
MESSAGEBOX(Macro) and see what the value of the string is.

Best Regards,
Scott
MSc ISM, MIET, MASHRAE, CDCAP, CDCP, CDCS, CDCE, CTDC, CTIA, ATS, ATD

"I try to be nice, but sometimes my mouth doesn't cooperate.
 
I don't know whether you can actually put a WITH inside a macro. But regardless of that, the immediate cause of the error is that there is no ENDWITH. I suggest you fix that first, then see what you have got.

The next step would probably be to dispense with the macro completely. I can't off-hand see why you need it. As far as I can see, it should be enough to loop through all the line objects, setting its position as appropriate. That said, I don't really understand what you are trying to achieve, so it might be more complicated than I think.

Mike

__________________________________
Mike Lewis (Edinburgh, Scotland)

Visual FoxPro articles, tips and downloads
 
Will start with an apology;

When typing the code into my original post I didn't put the ENDWITH in, but it is there in my code just before the ENDIF command, so is not that, sorry.

This is an IoT project, I have a building that has wireless nodes installed in it, the user can identify the nodes in the building via a custom RF interface, when this process happens I create database record that holds a load of data about the RF node, as there are different types.

When you load the GUI layer, it puts a pre-loaded drawing of the building on a form and dynamically creates a load of shape objects, one for each RF Node. The Nodes could be called 7020, 9045, 2364 etc. so when I create the shapes I call them shpNode7020, shpNode9045, shpNode2364 etc. can then zoom in and out and drag the shapes around drawing and preform actions on them, so for instance if one of the nodes is in a light I can turn the light on and off by clicking on the SHAPE and sending out an RF packet. All working and looks great.

The nodes can be placed in groups, so you can turn a group on/off from a single RF packet. So on the screen I want to be able to show a group, I am doing this by creating line objects, and setting their end points from the center of each shape to the mean average location of all the shapes in the Group, so you get a star type formation.

So I cycle through all my objects in the container looking for the SHAPES, if they are in the Group I am looking for I get their TOP/LEFT values to work out the average position of them and create a LINE object with it's TAG value as the unique part of the SHAPE name '7020', '9045' ....

The bit of code above is to then move the LINES around with the SHAPES. The code I am using makes sense to me, just it doesn't like the WITH command being executed via a Macro. I'm not sure how else to use the SHAPE when the link between the two objects is part of the name without loads of code and loads of Macros. When I did this before in VB6, many years ago, it would have been a case of;

FOR x = 1 to Number-of-Lines
LINE(x).X = SHAPE(x).X - (SHAPE(x).WIDTH / 2)
LINE(x).Y = SHAPE(x).Y - (SHAPE(x).HEIGHT / 2)
LINE(x).X1 = AverageX
LINE(x).Y1 = AverageY
NEXT

To set a point to the center of the shape as you could create and reference objects as arrays, which you don't seem to be able to do in VFP

Will end with an apology as well after probably going into too much detail to explain what I am trying to do ....
 
I think the discussion about WITH/ENDWITH in a macro substitution is secondary, as macro substitution compiles one line of code, I think code having WITH inside the macro disqualify, you could create a whole script and execute it with EXECSCRIPT().

You surely can do partial macro substitution of code having both WITH and ENDWITH so it at least compiples before it's even eecuted. Then the object part of the WITH line can vary per macro substitution. But I also don't see you do something that you would do inside WITH...ENDWITH.

In the end you could do whatever you want to do with a collection of objects by using a collection.

Whenever you create the shapes on your form, you can put them into a collection and then you can loop them.



Chriss
 
One possibility is to put your different objects into collections and then loop through those collections with FOR EACH. You could also just take advantage of the existing Objects collection that's inside every container and check Class or Baseclass to be sure you operate on the right ones.

I wrote a paper about collections in VFP:
Tamar
 
Chris/Tamar,

Thank you for your replies. The form creates the shapes when it loads, the lines are used to represent the groups, so they are created and destroyed as you want to view view different Groups, an RF node (shape object on the screen) can belong to several groups so it is not a one to one relationship between the shape and the line, hence using the TAG property of the line to create the association.

I do have this working, but the code is not elegant and am sure it can be done better so will look Tamar's document on collections to see if that works.

Regards,

Darren

 
What a collection mainly does for you is to store the references to the controls, which makes them easy to iterate. You already know the Objects collection of VFP forms (and also containers) and iterate over it checking that the left part of the object name is "LINE". Well, you don't have to do that as you can just put the lines into a collection, then only lines are in your collection and you have one less check. That gets rid of that aspect.

If you have relations between objects, you realize them with properties that are referencing the related object or objects, so a line object could have a square1 and quare2 property that refer to the two squares this line connects Just like a record that has two foreign keys, like a person could have a relation to monther and father also in the database with a motherid and fatherid. You could even implement binding to changes of a square position with bindevents, so anytime the position of a square changes the line objects change to connect to some defined edge or corner and stick to it automatically.

That is needing not only a collection, but the collection is the answer to your question of how to manage an array of controls when VFP does not support that on the level of a form object. There are some such colection with optiongroup and buttongroup, but that's not a general concept available for any control, just options and buttons. And options don't exist as single object, too, as they work inn groups and only one of the group is active, so VFP had to do such a group control for that case.

It doesn't really matter, as you can organize this in a collection or - by the way - also an array each square and line are added to while you generate them. This could be part of their init code if you design design classes for them. Then you also can add any properties for information like associations and are not limited to just the tag property. Use OOP and classes for your needs is the most general advice about this. And overcome the missing features by organizing yourself with what you have at hand.

I guess Tamars article will show it, collections are in short enhanced arrays, as you can not only address them by an index number but also a key, they also can grow and shrink easier than an array, there are some ugly implementation details about ADEL, to point out just one weakness detail of arrays. One specialty that won't apply to our case is that form objects in collections automatically remove themselves from the collection if the form is released. As you put shapes into your collection or collections or whatever else controls you'd use in the future, thse references are just as any object reference stored in parallel, they are kept there additionally to the original reference you have with the full name path like thisform.container1.shape32. Like any object, also controls will destroy when all references to it are removed, there is a reference counter behind the scenes, that VFP maintains. That bears a little risk to get difficulties releasing the form when the references to shapes are still in the collection(s) you use.

But make these additional collections properties of the container or form and the form release will still work flawless as both the form controls are released as always and the collections are released as part of the form and nothing remains of it that would cause a dangling reference hindering the form to close.

Chriss
 
Hi Chris,

Thanks for the extra detail. I had managed to get part way down this route myself as you suggest, the shape and line objects I have classes for and add various properties to them when they are created, such as 'real size' as this reference is needed to scale the object against the background image when I zoom in and out for instance.

I played with creating an array, and I obviously have a reference in the database to each shape as they are created from this when the form loads, nodes that have previously been added to the drawing. Am kind of happy with the collections concept, use them here and also when the form loads to cycle through all object to set properties against user preferences, font size, colour scheme of some objects etc., so all slowly starting to make more sense.

Am going to have a play over the next couple of days, try to organise the classes and collections in a more structured way. Appreciate the help from all so far.

Regards,

Darren
 
You know when you wake up in the middle of the night and think, how stupid am I, or is it just me that gets that?

Am still looking at using the collections in a different way as will speed up some of the object handling but, the answer to my original post is simple.

I was doing this, and it didn't work as the WITH statement was in the Macro so not complete.

FOR ox = thisform.CONTAINER.ConrtrolCount TO 1 STEP -1
IF LEFT(thisform.CONTAINER.Objects(ox).Name, 7) = "LINE"
_Macro = "WITH thisform.Container.SHAPE" + thisform.CONTAINER.Objects(ox).Tag
&Macro

thisform.CONTAINER.Objects(ox).Left = .Left + (.Width / 2)
thisform.CONTAINER.Objects(ox).Top = .Top + (.Height / 2)
ENDIF
NEXT

But if you just do this instead it works;

FOR ox = thisform.CONTAINER.ConrtrolCount TO 1 STEP -1
IF LEFT(thisform.CONTAINER.Objects(ox).Name, 7) = "LINE"
_Macro = "thisform.Container.SHAPE" + thisform.CONTAINER.Objects(ox).Tag
WITH &Macro
thisform.CONTAINER.Objects(ox).Left = .Left + (.Width / 2)
thisform.CONTAINER.Objects(ox).Top = .Top + (.Height / 2)
ENDIF
NEXT

[banghead]
 
Well,

your code still is missing an ENDWITH. And don't tell us that's somewhere after all this. You would get a nesting error if the ENDWITH is after the NEXT.
nesting_gvyeny.png


Theoretically it would be thinkable, as a WITH/ENDWITH block is not a branch of code in the usual sense, it's just a syntax shortening what you write within it and thus would not need to comply with the usual nesting of code. Anyway, that's what VFP tells about such mis-nested code where WITH begins within a FOR loop but ends after the NEXT or ENDFOR.

Here's an advantage of actually using an object variable - with a short name like obj:
Code:
FOR ox = thisform.CONTAINER.ConrtrolCount TO 1 STEP -1
IF LEFT(thisform.CONTAINER.Objects(ox).Name, 7) = "LINE"
_Macro = "thisform.Container.SHAPE" + thisform.CONTAINER.Objects(ox).Tag
obj = &_Macro
thisform.CONTAINER.Objects(ox).Left = obj.Left + (obj.Width / 2)
thisform.CONTAINER.Objects(ox).Top = obj.Top + (obj.Height / 2)
ENDIF
NEXT

Chriss
 
Cris Miller said:
_Macro = "thisform.Container.SHAPE" + thisform.CONTAINER.Objects(ox).Tag
obj = &Macro

Why is the variable in the first line above nammed _Macro but the second line is &Macro?



If you want to get the best response to a question, please check out FAQ184-2483 first.
 
Chris,

As per earlier post I missed out the ENDWITH when I typed the pseudo code into the forum originally, it is just before the ENDIF, and when I did the update I cut and pasted the original code.

This is the actual code that I have;

Code:
FOR ox = thisform.ConArea.ControlCount TO 1 STEP -1
 IF LEFT(thisform.ConArea.Objects(ox).Name, 8) = "lneGroup"
   _Macro = "thisform.conArea.shpProduct" + thisform.ConArea.Objects(ox).Tag
   WITH &_Macro
     _CentreX = _CentreX + .Left + (.Width / 2 )
     _CentreY = _CentreY + .Top + (.Height / 2 )
     _CentreN = _CentreN + 1
   ENDWITH
 ENDIF
NEXT
_CentreX = (_CentreX / _CentreN)
_CentreY = (_CentreY / _CentreN)


FOR ox = thisform.ConArea.ControlCount TO 1 STEP -1
 IF LEFT(thisform.ConArea.Objects(ox).Name, 8) = "lneGroup"
   SEEK VAL(thisform.ConArea.Objects(ox).Tag) ORDER TAG ProductID IN dnProducts
   _x = _CentreX - 10
   _y = _CentreY - 10
   WITH thisform.ConArea.Objects(ox)
     _Macro = "thisform.conArea.shpProduct" + thisform.ConArea.Objects(ox).Tag  + ".Left + (thisform.conArea.shpProduct" + thisform.ConArea.Objects(ox).Tag  + ".Width / 2 )"
     _x1 = &_Macro
     _Macro = "thisform.conArea.shpProduct" + thisform.ConArea.Objects(ox).Tag  + ".Top + (thisform.conArea.shpProduct" + thisform.ConArea.Objects(ox).Tag  + ".Height / 2 )"
     _y1 = &_Macro
     .LineSlant = IIF((_x > _x1 AND _y > _y1) OR (_x < _x1 AND _y < _y1), "\", "/")
     _x = IIF( _x > _x1, NVL(_x1, _VFP.DoCmd("_x1 = _x")), _x)
     _y = IIF( _y > _y1, NVL(_y1, _VFP.DoCmd("_y1 = _y")), _y)
     .Left = _x
     .Top = _y
     .Width = _x1 - _x
     .Height = _y1 - _y
     .Visible = .T.
   ENDWITH 
 ENDIF
NEXT

Regards,

Darren
 
I think I stop wondering about details and simply accept this is now doing what you want.

But let me address the idea about collections once more: At the time you create the lneGroup controls the same way you do (not posted) you cn put them into a collection and this collection therefore only has thos controls, you will not need to iterate all Object and check whether their name begins with 'lneGroup'.

When you have that collection the FOR loop becomes:
Code:
FOR EACH loLine in Thisform.colLines
...
NEXT

It's actually not shortening all this very much, but it removes the need to check IF LEFT(thisform.ConArea.Objects(ox).Name, 8) = "lneGroup". Likewise you would add a colShapes or colProductshapes collection.


Chriss
 
Hi Chris,

Nodes are created like this;

Code:
SELECT dnPGroups
 SCAN FOR GID = thisform.cmbGroups.ListIndex
    WITH thisform.conArea
     cName = "lneGroup" + ALLTRIM(STR(dnPGroups.NodeID))
     .NewObject(cName,"lneGroup","DexNet")
     cComm = "thisform.conArea." + cName
     WITH &cComm
      .Zorder(1)
      .BorderColor = IIF(dnPGroups.Pos = 1, RGB(50,50,200), RGB(200,50,50))
      .Tag = ALLTRIM(STR(dnPGroups.NodeID))
     ENDWITH
    ENDWITH
ENDSCAN

I had the code working a long time ago, but as per original post I was not happy and knew that it could be done better. I originally had lots of macros, lost about half the code to get me to where I am at the moment. I am still looking to implement the collections, as this makes sense to me, but when it was suggested it was a new concept that I needed to understand, much the same as Tamars business class references in his paper I am going trough.

Have not accepted the code or abandoned collections, just going through the evolution of understanding and implementing them.

As always, thank you for your time and response [smile]

Regards,

Darren
 
Sounds like my papers are helpful to you. Glad to hear it.

Just BTW, I'm a "she," not a "he." (Tamar is a Biblical name.)

Tamar
 
Hi Tamar ,
'our' crazy project is using also your helpful goodies and tricks , so you have the rights and a place of honor , can join for free if you wish , (It is free anyway) :)

okarl
 
Ho D-Ward,

thanks for posting that code section. So a collection comes into play this way:

Code:
With Thisform.CONAREA
   [highlight #FCE94F].AddObject('colLines','Collection')[/highlight]
   * could also be a class based on collection, in which case you use NewObject.
   Select DNPGROUPS
   Scan For GID = Thisform.CMBGROUPS.ListIndex
      CNODEID = Alltrim(Str(DNPGROUPS.NODEID))
      CNAME = "lneGroup" + CNODEID
      .Newobject(CNAME,"lneGroup","DexNet")
      CCOMM = "thisform.conArea." + CNAME
      [highlight #FCE94F].colLines.Add(&CCOMM,CNODEID) && adding the line object reference to the collection[/highlight]
      With .colLines.Item(CNODEID)
         .ZOrder(1)
         .BorderColor = Iif(DNPGROUPS.POS = 1, Rgb(50,50,200), Rgb(200,50,50))
         .AddProperty('NodeId',DNPGROUPS.NODEID) && instead of storing that in the TAG property add your own.
         .AddProperty('cNodeId',CNODEID) && and in string form, if you need that, too.
         && .Tag = Alltrim(Str(DNPGROUPS.NODEID))
      Endwith
   Endscan
Endwith

A class should be able to care about itself, i.e. regarding your earlier code to compute an overall center position compute its own center.So if you make your own more specialized collection class specifically for the lines or lneGroup nodes, that could have a method computecenter to compute the overall center of a collection of nodes and the lneGroup class you already have could also be extended with a computecenter method to compute its own center. Or a centerx and a centery property at least, which compute themselves in a centerx_access and centery_access method. If you already use OOP, then also use it to let each class have the ability to provide the information about it you actually need from outside, instead of using just its native properties from outside - left, top, width, and height. It's even simpler to see with the tag property. Simply add a property you want to have. At runtime with AddProperty or even better in a class in its design.

In your code you have
Code:
SEEK [highlight #FCE94F]VAL(thisform.ConArea.Objects(ox).Tag)[/highlight] ORDER TAG ProductID IN dnProducts
That will become
Code:
SEEK [highlight #FCE94F]thisform.ConArea.Objects(ox).NodeId[/highlight] ORDER TAG ProductID IN dnProducts
You turn a number into a string and later need the number and get it with VAL. Well, just store exactly what you need into your own new nodeid property. You could even add all fields of the DNPGROUPS record as properties to the lneGroup object with SCATTER NAME object ADDITIVE while DNPGROUPS is selected - and it is in a scan loop without any further SELECT.

Chriss
 
Status
Not open for further replies.

Part and Inventory Search

Sponsor

Back
Top