Dynamic UI Layout

I'm a bit new to Ignition, so bear with me. I've done a fair amount of programming in Java and some script automation through JavaScript which included interface windows (mostly Photoshop batching automation with input popups). That said, trying to learn this combination of Visual layout with Python/Jython Scripting support is a little jarring. The IA tutorials help a lot with understanding how Ignition can be used, but I have a habit of jumping into the deep end to actually learn how to swim.

Anyway, what I'm trying to do is dynamically layout a page in a way similar to a table based on a varying length dataset. In fact, the only reason Power Table won't work is the lack of a multi-row header with mergeable cells (like the data cells in it can be) as well as the lack of ability to change the input type of the last column to another component (switching between textfield, numerical field, dropdown, or checkbox as needed).

The number of columns I need is static, but the rows of data are obviously not, and the first and second columns will have a varying number of cells merged together, such as the example below:


I've created this same layout in Java using nothing more than labels and input fields in a GridBagLayout (seems to be Java's take on MigLayout), expanding the first and second columns based on the number of items contained "under" (to the right of) them; basically like a tree-table. That said, the only way I can see doing that here is via Templates and Template Canvases with embedded Template Repeaters. Trying to control the data within each row starts to look like it would be a bit of a headache, though, and the columns of the repeaters don't match the width of the columns in the headers without a ton of extra work [mostly just trying to figure out how] to set their size and position to match the header while still butting up to their neighboring cells (this part's more my lack of experience with Ignition, I'm sure).

If I could use the MigLayout to dynamically add Labels and input fields to a container would be best, as then I wouldn't have to nest anything except the underlying data structure. Unfortunately, I only see a way to supply MigLayout options to the Template Canvas objects and no way to create objects at runtime with script. Am I just looking in the wrong places? Is it even possible to build a layout such as this with script at runtime, or am I stuck with template nesting to try and achieve this?

And for those that would ask why I don't just modify my table to fit the power table... there's a mound of paperwork behind switching the form's "Look & Feel" to fit the power table that looks more daunting (and less productive) than learning to use this software better :wink: That said, if power table can be modified to fit, I'm open to advice. Thanks for any help!

This is supported by the underlying library (Jide Grids): https://www.jidesoft.com/products/JIDE_Grids_Developer_Guide.pdf, "just" not exposed to you. Sufficient hacking of a power table should support this fine, probably via the initialize hook.

This would also be possible with a custom cell editor and probably renderer, supplied via configureEditor (or manually injected via initialize, but the extension function is probably better).

Your understanding on both of these points is correct.


I actually don't know which approach I would go with if I were tasked with creating this from scratch; the power table or the template canvas absolutely could do this. Knowing your constraints would make certain things simpler - e.g. are data cells only ever "spanned" vertically, or could they span horizontally as well? How fixed are the columns and their general pattern - i.e. is the number of rows going to change but you're always going to follow that basic pattern? It would be pretty easy to lay that all out with a single template canvas, if that's the case, and each data column as an individual template repeater.

2 Likes

You've marked this post with the Perspective Tag, but the Power Table is a vision component. Are you attempting to do this in Vision or Perspective?

Digging into the JIDE is something I'd be open to, since I'm familiar with extending Java objects and working with Abstracts to create my own, or overriding the table methods to alter its behavior, but I doubt our IT department wants me doing that in the position I currently hold. And while I do jump in the deep end, I'm not ready to get my self in trouble jumping into the ocean. :sweat_smile:

As far as the constraints, only select header cells are spanned horizontally, the data cells underneath only every span vertically in the first or second column. The header is static, unchanging data, so even the labels of the header could be named pre-runtime. The data underneath the header is the only thing that changes at runtime. The amount of data under the header will also change, as will the number of groups, such as "Data Group 1" or "Data Group 1.1". That said, there will always be at least one "Data group 1" and "Data group 1.1". Whether or not there is a "Data Group 2" or "Data Group 1.2" depends on the dataset being read.

To start, the data will likely be read in via CSV, but later on will be DB records. Below would be an example based on the table previously linked, in CSV form:

Data Group 1,Data Group 1.1,Data 1.1,Data 1.2,Data 1.3,Data 1.4,Data 1.5,Data 1.6,Data 1.7,Input 1
,,Data 2.1,Data 2.2,Data 2.3,Data 2.4,Data 2.5,Data 2.6,Data 2.7,Input 2
,,Data 3.1,Data 3.2,Data 3.3,Data 3.4,Data 3.5,Data 3.6,Data 3.7,Input 3
,Data Group 1.2,Data 4.1,Data 4.2,Data 4.3,Data 4.4,Data 4.5,Data 4.6,Data 4.7,Input 4
,,Data 5.1,Data 5.2,Data 5.3,Data 5.4,Data 5.5,Data 5.6,Data 5.7,Input 5
,Data Group 1.3,Data 6.1,Data 6.2,Data 6.3,Data 6.4,Data 6.5,Data 6.6,Data 6.7,Input 6
,,Data 7.1,Data 7.2,Data 7.3,Data 7.4,Data 7.5,Data 7.6,Data 7.7,Input 7

So basically, the vertical merging of a cell in the first or second column is based on the number of rows [beneath a row with data in that column] that are blank. When there's a new row that has data in the same column, a new vertically merged cell starts and the prior one ends.

Sounds like what I'm aiming for. I was just worried the data in the columns might not match up row-by-row, but I'm probably overthinking how repeaters work (I tend to overthink things, a lot :sweat_smile:)

My apologies. I'm still getting familiar with what is part of what toolset. Also a little fuzzy as to the difference between the 2. I'm under the impression that Vision is meant more for web/mobile while Perspective is more desktop application... that said, I just did a google for it and it seems I have that backwards?

Edit: Looking at that post I found, it seems I need to spend a little more time getting to understand the difference, as it also affects performance... *Sigh* more things to research, lol.

1 Like

I think it's the other way around

Google is correct, Perspective is the best/only solution for Web/Mobile development and has a skill threshold that works around that paradigm (HTML, CSS, JavaScript, Python/Jython).

Where as Vision is a more traditional SCADA based toolset. with a skill threshold based on Industrial standards, Java, and Python/Jython.

Perspective is all processed on the Gateway and so Client File access without operator intervention is basically non-existent (at this time anyway).

Vision runs in a client and allows far more OS access because it isn't restricted by browser securities.

4 Likes

For some reason, re-reading this part seems to have cleared some unrelated info out that was jamming up the gears... Damn you ADHD! :rofl: I'll plug away a little more when I get some free time to do so.

Edit: Taking a step back also revealed some inefficiencies I'd put into it that I just worked out. Ahh the growing pains of "getting the lay of the land", so to speak (ie: learning how everything is connected). Went from about 20 templates down to 2 to achieve the same result :blush:. Easy to let optimization go to the wayside when you've got tunnel vision focusing on a specific problem.

1 Like

Out of interest, how would merging of cells vertically be best achieved in Perspective?
@cmallonee perhaps?

Eg this part

I wanted to do this for a project but put it in the way-too-hard basket, but I was always curious

CSS grid layout is probably the most straight forward option.

So I added the base canvas to a window with a button setup to print the template dataset to console when I activate it (so I can try and figure out how to structure it). I can retrieve the canvas object and verify that it is of the type "TemplateHolder", but when I try to get the list of templates within it, I get an error.

par = event.source.parent
print type(par)
canv = par.getComponent("Template Canvas")
print type(canv)

Which prints this to console:

<type 'com.inductiveautomation.factorypmi.application.components.BasicContainer'>
<type 'com.inductiveautomation.factorypmi.application.components.template.TemplateHolder'>

However, adding the command .getAllTemplates() or even .getTemplates() onto the "canv" variable, I end up with the error:

AttributeError: 'com.inductiveautomation.factorypmi.application.com' object has no attribute 'getAllTemplates'

along with the rest of the usual java stacktrace. This doesn't seem to match up with what's mentioned here: IA Template Canvas Documention
I'm guessing I'm not drilling down into the right layer to retrieve the templates using this method? What am I missing? And yes, I'm currently working with version 7.9.

I was able to get the dataset by adding script to the canvas' initializeTemplate function

templates = system.dataset.toPyDataSet(self.getTemplates())
print templates[0][0]

, but this runs once for every sub-template added to the canvas.

Any suggestions? I feel like I'm missing something simple here...

Edit: I'm guessing the error is due to some Java to Python (Jython?) conversion I'm not familiar with.

You don't have a template canvas at all. TemplateHolder is the class you get if you just have a regular template instance dropped onto the window.

1 Like

Figured it out... though now I feel dumb for how long it took me... lol.

par = event.source.parent #Get the event source; aka the button
templateContainer = par.getComponent(canvasName) #Get the container holding the template
template = templateContainer.getComponent(0) #Get the template holding the Template Canvas
canvas = template.getComponent(0) #Get the Template Canvas object
canvasTemplates = canvas.getAllTemplates() #Get a list of all templates loaded into the Template Canvas
canvasDataset = canvas.templates #Returns the dataset used to construct the rows/columns of the embedded templates

I had only tried drilling in one step deep before, which is why I still got the error... Helps when I check the data type a little closer using .type()!

Edited for ease of readability in case someone else stumbles upon this issue.

GOT IT!

I was able to replace an existing dataset in an embedded Template Canvas using this code:


par = event.source.parent #Get the event source; aka the button
templateContainer = par.getComponent(canvasName) #Get the container holding the template
template = templateContainer.getComponent(0) #Get the template holding the Template Canvas
canvas = template.getComponent(0) #Get the Template Canvas object

#Header MUST match the dataset columns seen inside Templates > Dataset Viewer of the property editor for the template canvas
header = ["name","template","layout","x","y","width","height","parameters"]
#Each row must contain data (even if that data is null/None) for each of the header columns 
rows = [["Head 1","LabelTemplate","cell 0 0 1 2,h 60px,w 50px",0,0,None,None,'{"labelText":"Head 1"}'],
["Head 2","LabelTemplate","cell 1 0 1 2,h 60px,w 50px",0,0,None,None,'{"labelText":"Head 2"}'],
["Head 3","LabelTemplate","cell 2 0 4 1,h 20px,w 600px",0,0,None,None,'{"labelText":"Head 3"}'],
["Head 3.1","LabelTemplate","cell 2 1 1 1,h 40px,w 50px",0,0,None,None,'{"labelText":"Head 3.1"}'],
["Head 3.2","LabelTemplate","cell 3 1 1 1,h 40px,w 50px",0,0,None,None,'{"labelText":"Head 3.2"}'],
["Head 3.3","LabelTemplate","cell 4 1 1 1,h 40px,w 50px",0,0,None,None,'{"labelText":"Head 3.3"}'],
["Head 3.4","LabelTemplate","cell 5 1 1 1,h 40px,w 450px",0,0,None,None,'{"labelText":"Head 3.4"}'],
["Head 4","LabelTemplate","cell 6 0 1 1,h 20px,w 100px",0,0,None,None,'{"labelText":"Head 4"}'],
["Head 4.1","LabelTemplate","cell 6 1 1 1,h 40px,w 100px",0,0,None,None,'{"labelText":"Head 4.1"}'],
["Head 5","LabelTemplate","cell 7 0 1 2,h 60px,w 75px",0,0,None,None,'{"labelText":"Head 5"}'],
["Head 6","LabelTemplate","cell 8 0 2 1,h 20px,w 100px",0,0,None,None,'{"labelText":"Head 6"}'],
["Head 6.1","LabelTemplate","cell 8 1 1 1,h 40px,w 50px",0,0,None,None,'{"labelText":"Head 6.1"}'],
["Head 6.2","LabelTemplate","cell 9 1 1 1,h 40px,w 50px",0,0,None,None,'{"labelText":"Head 6.2"}'],
["Head 7","LabelTemplate","cell 10 0 1 2,h 60px,w 200px",0,0,None,None,'{"labelText":"Head 7"}']]

#Set the Template Canvas' dataset to the one listed above
canvas.templates = system.dataset.toDataSet(header,rows)

This ONLY creates the header for the example I gave in the OP, but the data beneath it can be added in the same exact way... just add more rows with the appropriate data and MigLayout parameters. This can get pretty creative by simply adding more parameters to pass to the sub-template you're using to create the layout and/or use additional templates. In my example, I just use a single sub-template named "LabelTemplate" with one parameter "labelText".

My sub-template actually has a second parameter I don't use, yet. But, I could use it to do something like below, to set the alignment of the text within the label's available area to start on the left side of the label area:

["DataGroup1","LabelTemplate","cell 10 0 1 2,h 60px,w 200px",0,0,None,None,'{"labelText":"Data Group 1","labelHAlign":2}']
1 Like

You shouldn't need all of those individual cell directives, for what it's worth. It's almost always best to 'flow' your stuff into miglayout. E.G, I would construct your layout something like this:

  • Header 1: "spany 2"
  • Header 2: "spany 2"
  • Header 3: "spanx 4"
  • Header 4
  • Header 5: "spany 2"
  • Header 6: "spanx 2"
  • Header 3.1
  • Header 3.2
  • Header 3.3
  • Header 3.4
  • Header 4.1
  • Header 6.1
  • Header 6.2

The span directives should automatically fill the gaps and prevent you from needing almost any manual layout stuff - that's why I asked about how dynamic the structure really had to be. Miglayout is really good at this stuff :slight_smile:

Highly recommended, if you don't already have it bookmarked:
https://www.migcalendar.com/miglayout/mavensite/docs/cheatsheet.html

3 Likes

I will give that a try and see how it works out. Moving forward, that sounds like it will be a lot easier to write code for laying out the data from the CSV than the cell method. Although cell is a bit quicker to "read" how it's laid out with static data, I'm now to the point of being more concerned with the result and efficiency of dynamic data. So thank you for that tip! I probably would have found it way later, after writing some extra code I don't really need to write, lol.