If your users operate in different system languages then you need to consider what happens to your formulas when a document, authored in one language, is edited in a different one.
A common scenario in Visio is that you want to have a shape change color based on some state - usually in Shape Data.
There are lots of approaches to solve this, two of the main ones employing the IF
and INDEX
functions.
For both of these options you might add a Shape Data cell that holds the value you want to map to, Prop.Status
, and also an index cell, User.StatusIndex
, that allows you to abstract the actual value to an index.
Using an IF
function you could create a lookup to match the index to a color like this:
This is a robust solution that will work perfectly across languages. However, even for three colors, it's not easy to read and therefore maintain and, if you start to add more colors, the problem just gets worse. (For the screenshot I've added line breaks to make it more readable, but in the ShapeSheet you'll see this as a single line.)
The alternative then, is to use the INDEX
function.
Here you add an intermediate cell to hold the palette of colors, User.Colors
, and User.Color
references the status index to select just one of the colors from the palette list.
This also works well as long as your users are working with a single language. There's a problem lurking, however, if that's not the case.
Suppose that you create a document in an English based system (en-UK) and then open it on a French one (fr-FR). Although Visio changes the local RGB
functions to use French comma separators instead of English semi-colon ones, a CellChanged
event is not fired and so the result is not re-evaluated. This means that you have a string representation of the RGB
function that cannot be evaluated and so the resulting fill color of the shape becomes the default: 0 (black).
So why is this happening?
When opening a document in a non-English language, Visio converts some formulas from their English (US), or universal, representation to a their local one.
The ShapeSheet only shows universal syntax, but the underlying cells have local and universal syntax, and this applies to both the formula and result properties.
For example, here's the same shape that you looked at above and I've written some quick code, using LINQPad, to report the Formulas and Results for three cells. This is what it looks like with the system language set to English.
Note that the RGB argument separator is a comma.
If you then open the document with French set as the system language, you'll see this:
Notice now that the local Formula
(not FormulaU
) has changed to use the French argument separator, the semi-colon, and the universal FormulaU
keeps using the comma as before.
Also notice that the local ResultStr
(not ResultStrU
) continues to use the comma argument separator and so when User.Color
looks up the color in the User.Colors
list it also retrieves an RGB function string with those incorrect separators.
At this stage no changes have been made to the Shape Data and the FillForegnd
cell remains red, as it should be.
The problem becomes visible when you attempt to edit the Status property - User.Color
is re-evaluated, but with a string that FillForegnd
can't parse and so the cell now defaults to black.
What appears to happen, is that when Visio opens a document in a non-English language it will attempt to convert some local formulas based the cells data type or units.
For string type cells Visio doesn't know what the purpose of the cell is, and so (I believe), doesn't fire a change event.
The type of a cell is really the result of the section that it's housed in and its formula that, in turn, depends on the functions and operators used within the expression.
RGB
, for example, returns a color (visUnitsColor
) but the ampersand operator (&
), used to concatenate the list returns a string and so User.Colors
is also a string (visUnitsString
) type.
The INDEX
function returns a string (visUnitsString
) and so User.Color
is, again, a string.
So, if the problem is that User.Colors
doesn't see a changed when language change occurs on open then we need some way to trigger the re-evaluation when the Shape Data is changed.
To achieve this you can append the Shape Data cell (Prop.Status
) to the end of the palette list and this ensures that every time that data cell changes so the colors list will also re-evaluate and pickup the current local RGB syntax.
Here's an example of the amended shape opened and edited, with French set as the system language. You can see the correct color is interpreted from the local string representation in User.Color
, to its local and universal counterparts in FillForegnd
:
So, to ensure color lists and the INDEX
function continue to work across languages just append a reference the data cell they're associated with to ensure that updates happen correctly.
Visio has a number of ShapeSheet transform functions that allow you to transform a point expressed in one shape's coordinate space to the coordinate space of another one.
When you open the ShapeSheet in Visio you're presented with coordinates expressed in two ways - Local and Parent coordinates.
Most cells use Local coordinates (see green shading above) and these are values that are local to the shape's bottom left origin (x:0, y:0).
The others, PinX/Y and Angle (shown in blue), describe the position and orientation of the shape in the coordinate space of its Parent container. This container may be the page, for a top level shape, or a group shape if it's a sub-shape.
While a shape's Pin represents its position within a container (ie its Parent position), the LocPin (or Local Pin) can be seen as an offset for the shape's drawing origin.
For example, in the following image the blue rectangle's Pin is set at x:40, y:40 in Parent coordinates, which in this case is the page. The shape's Local Pin is set to the default middle of the the shape at x:30, y:20 (see red cross-hair). Note, normally Visio uses relative formulas - I'm just using absolute values here to make it easier to see what's happening.
The rectangle is also a group shape and contains a single orange circle sub-shape. The Pin position of the sub-shape is set to x:40, y:30 and this is described in Parent coordinates, which is the rectangle.
If you then shift the rectangle's LocPin to x:20, y:10 then you can see that the entire local coordinate space of the of the rectangle has moved. Note though, that the shape's Pin position, relative to the page, remains the same, as do the local coordinates of the (orange) sub-shape.
So now we're clear on Local and Parent coordinates, let's move on to looking at the transform functions.
Visio has six main functions to help with changing from one coordinate system to another. Two for dealing with angles, which I'll come onto in minute, and four for general transformation.
LOCTOLOC
and LOCTOPAR
allow you to specify both the source and destination coordinate spaces via cell references, neither of which have to be in the shape you're defining your formula in. You can think of LOC
and PAR
, on the other hand, as shortcuts that allow you to assume the destination coordinate space is the one in that you're defining the formula in.
Docs description: Takes a point defined in one shape's local coordinates and returns the equivalent point expressed in the local coordinates of the shape associated with the formula.
Example usage: If you look in the "Blocks with Perspective" stencil you'll find that each shape references a vanishing point defined on the page. The shape contains a control that uses the LOC
function to convert the page level point to one that's described in the Local coordinates of the respective shape.
Docs description: Returns the x,y coordinates of a point in the coordinate system of the shape's parent.
My description: Takes a point in the Local coordinates of a shape and converts it to the Parent coordinate space of the shape where this function is used.
Example usage: Any time you use point to point glue with a connector (ie statically gluing a 1D endpoint to a connection point, rather than dynamic whole shape glue) Visio injects formulas into the connector endpoint cells and uses the PAR
function to convert the Local coordinates of the connection points to the Parent coordinates it needs to locate the connector on the page.
Docs description: Returns a transformed point in local coordinates in the destination coordinate system.
My description: Takes a point in the Local coordinates of a shape and converts it to the Local coordinate space of another shape.
Example usage: I couldn't spot an example of LOCTOLOC
in the built in stencil content, so here's a slightly contrived example - consider the following image where you've got a rectangle (Sheet.3) with an (orange) sub-shape. You want to locate the sub-shape over the connection point in the other rectangle (Sheet.1), so in the ShapeSheet of the sub-shape (Sheet.4) you use the LOCTOLOC
function to transform the Local coordinates of the connection point to Local coordinates within the group shape - Sheet.3. The result is that sub-shape will remain positioned over the connection point irrespective of where its group parent is placed on the page. One benefit of this arrangement is that, although the orange sub-shape appears to be a child of the the upper rectangle (Sheet.1), it will be deleted when its parent (Sheet.3) is removed from the page allowing for an interesting visual attribute type relationship.
Docs description: Returns a transformed point in parent coordinates in the destination coordinate system.
Example usage: If you use static (point to point) glue to connect two shapes together, Visio injects a formula into the PinX/Y cells and makes use of the LOCTOPAR
function. In the image below you can see the PinX cell for the (green) Decision shape and that the point being transformed is the Local Connection point in the (blue) Process.4 shape. It's using Process.4 as the source coordinate space via a reference to the Process.4!EventXFMod cell, and this shape (Decision) as the destination coordinate space via another reference to the [Decision.]EventXFMod cell. (The outer PNTX
function just pulls the x component out of the point and there's also the addition of 12.5 mm, which is half the shape's width.)
Docs description: Returns a transformed angle in the destination shape's parent coordinate system. Converts an angle from local coordinates in a source shape to the parent coordinates in a destination shape.
Example usage: When you add a connection point to a shape in Visio via the UI, the point is given a direction. The direction is set via a vector, which, by default, is perpendicular to the vertex that the connection is applied to. You can see this in the following image where the lower ShapeSheet shows the cells for the green circle.
I've added a second geometry section (see diagonal green line) and set the end of the line to the DirX/Y cells of the first connection row so that you can visualize that vector. The image shows the blue rectangle mid-drag, and you can see that the rectangle rotates to the angle determined by the vector. Visio then takes this angle and uses the ANGLETOPAR
function to transform the degree value from the Local value in the coordinate space of the green circle, to Parent coordinate space of the rectangle.
Docs description: Returns a transformed angle in the destination shape's local coordinate system. Converts an angle from local coordinates in a source shape to the local coordinates in a destination shape.
ANGLETOLOC
is broadly the same as ANGLETOPAR
just that it returns coordinates from a Local space rather than the Parent.
So I hope this helps in understanding how this set of functions work. The key thing to think about when using them is where is your calculation going to be consumed? Is it a cell that uses Local or Parent coordinates? Note that the consuming cell is not necessarily where the function is defined.
When creating a set of colors that work well together you often want to change the saturation and luminosity levels on all of the colors at once. There are some amazing color tools out there, but up to now I've not found any that lets me do this en-masse adjustment.
So I thought I'd try and create a utility tool in Visio that lets you do this.
When creating this tool I had two goals:
The first requirement can be easily met by standard ShapeSheet logic, but the second requires some code.
Bearing in mind that not everyone will be able to run macro code, I've created two files: a template (.vstx) without code and an accompanying stencil (.vssm) where the code lives.
The stencil file is digitally signed from visualsignals ltd and if any changes made to the stencil this will invalidate the signature.
If you're not familiar with it, HSL is an alternative color model to the more common RGB. RGB can be thought of as a cubic arrangement, with each dimension representing a range for each component. HSL, on the other hand, represents colors in a cylindrical model, which means that each component (hue, saturation and luminosity) can set as a single value.
SharkD, CC BY-SA 3.0, via Wikimedia Commons
You can read more about HSL on Wikipedia.
The solution has two masters:
a Color
circle shape for each color instance
and a Container
shape to visualualize the Hue and Saturation components + a slider to control the Luminosity of all colors on the page
When dropping Color
instances onto the page the resulting HSL values are determined by each shape's vertical (Saturation) and horizontal (Hue) position.
As these are standard Visio shapes you can drag select, Ctrl+Click and Ctrl+A as you like and then move the selection on the page.
Each of the shapes reference a page level Shape Data cell to retrieve their Luminosity value and this can be set by either directly editing the Shape Data window or by dragging the yellow control handle on the Container shape, which is visible when the shape is selected.
On page right-click you can choose whether to display color values which, if turned on, will display the RGB, HSL and HEX values.
Although you control the global luminosity via the page, as described above, you might also want to add an offset to individual shapes and you can do this via a Shape Data row named Luminosity Offset
. This defaults to 0 but can take a negative or positive integer and the shape will add this to the global value in order to arrive at its own calculated result.
If Show color values is true then the offset will be displayed along side the other information.
The file contains a report definition that will report all colors in RGB, HSL and HEX formats, which you can run from Review / Shape Reports.
It's really there for those who are not able to run the macros version and as an alternative to the Copy color values (see below).
The code version of the document includes a few additional features:
If you've applied local luminosity offsets to a number of Color
shapes and want to reset them then you can do this via the Reset all local luminosity offsets page right-click menu item.
You can right-click on the page and select "Copy RGB values". This harvests the RGB string cell from each Color shape, concatenates this into a list and then copies the resulting string to the clipboard. So, for example, if you had three colors on the page you'd find something like this in the clipboard:
RGB(238,168,111)|RGB(113,236,122)|RGB(112,190,237)
You're free to change the parameters of the corresponding CALLTHIS function in the page ShapeSheet, where the last three arguments are: the target cell name, the color delimiter (pipe in the above example) and a boolean that determines whether to retrieve the local formula (with local component separator) or universal which will always be a comma.
On the Color
shape you'll find string cells cells:
User.RGBStr
User.HSLStr
User.HEXStr
Another right-click feature on the page is the ability to save the current set of Color
shapes to the document, which you can then 'reload' at a later date.
Individual named Palettes are stored in the Document's ShapeSheet and so are saved with the document.
Colors are stored in the palette as RGB values and then translated back to HSL when they're dropped back onto the page.
There are a number of options for converting from RGB to HSL and back again. I looked at a number of options for doing this:
In the end I went for the Win32 api which has a ColorRGBToHSL function, which does very nicely.
Now that you're storing palettes you need some way to manage them and so there is an further menu item named 'Manage palettes...'.
This surfaces a form which allows you to select one or more palettes and either drop all of the colors in each palette or, delete the selected palettes.
Note - If the luminosity level on the page has changed since the colors were stored a corresponding offset will be applied on drop.
This utility is specialized enough that I decided to catch the select all (Ctrl+A) event. Most of the time I just want to select the Color
shapes and not the Container
shape. However as I needed the Container
to be selectable, in order to move the luminosity slider, I couldn't just lock that layer. Instead the event is caught and quickly deselects the Container
resulting in a Colors only selection.
New pages - in Visio if your master shapes reference any page User or Shape Data cells then these cells are created on new pages if they don't already exist. That's not the case for Actions cells (which give you context menu functionality. One route around this would be listen for the page added event and then create the rows in code, however this won't work if you're using the non-macro template only. The alternative is to use the Duplicate
command on the page tab context menu and this will bring across all of the cells that you need.
This is a quick post on how radial gradient stops work in Visio.
In Visio, gradient stop positions are set as a percentage value:
If you're applying a linear gradient then that percentage is a position on virtual ray that has a start and end point and is determined by the FillGradientAngle
. So the following image shows a linear gradient with two stops (a white one at 0% and a blue one at 100%) and an angle of 0 degrees (shown by the orange arrow):
For a radial gradient the FillGradientAngle
is not used and the start end points are determined based on the value of the FillGradientDir
cell. Setting a value of 3
sets the begin point to the center of the shape and the end point becomes the corner of the shape.
Here's a shape that demonstrates this and uses a (yellow) control handle to set the gradient position. (I've also added another orange arrow to show the direction of the gradient ray)
Just in case you're wondering where the gradient part is in these solid colours, I've set adjoining stops to the same values, which produces a hard transition (ie no gradient). This is useful a) to see where the stop positions actually are and b) as a technique to allow for multiple colours in a single shape. You can see another example of this in a traffic light post I did a little while ago.
The stops in the above shape are defined as follows:
...and then User.PathPercentage
is calculated by taking input from the Control handle, pushing it into the User.RawX/Y
cells, working out the radius and angle and then using those values (User.CalcX/Y
) back in the Control handles. Here's the Control handle's XY cells:
The result is that you can change the position of the stops via a control handle in the shape:
One caveat to bear in mind is that you cannot lock gradient stops so you run the risk of users either deleting or overwriting the stops via the controls in the UI. One option to protect them is to lock formatting of the whole shape using the LockFormat
cell. It would be great to have a LockFillGradientStops
but alas that doesn't exist currently.
And finally, a very handy tip from Chris Roth - if you're having trouble selecting gradient stops that are close together in the UI, you can use the arrow keys to navigate between them, allowing you to not mess up the positions but set all of the other stop properties.
Occasionally, in Visio, you want to create a ShapeSheet reference to a cell in a shape that may not exist. For example, you might want one shape to behave differently depending on whether another shape is already on the page, such as a title block or some other container style shape. In my case, I want to be able to set a target shape ID and have the another shape track its position.
(Knowing the ID won't always be ideal, but it works for my debugging scenario, which I'll come onto in the next post.)
Here's a screenshot of five shapes. The connector, whose ID is 5, is the target shape the circles want to track.
I'll break this into two part - first I'll deal with getting a valid reference, and then I'll attack the location tracking part.
So, starting with the green circle shape - This technique starts off by the user setting the ID of the target shape via Shape Data (Prop.TargetId
). I've set this to 5, which, as I mentioned, is the ID of the connector shape. The Shape Data cell is referenced in User.CandidateNameID
cell. This in turn is referenced by the User.TargetTrigger
cell, so that when the value in User.CandidateId
changes the SETF
(Set Formula) function is fired.
SETF
pushes a formula to a given cell - User.CandiateId
and the formula that I'm pushing in is the string name Sheet.5 concatenated with the ID
function.
The way that SETF
works is that if the formula that you're pushing is syntactically correct then the formula will appear in the chosen cell as expected, but, if the formula is not correct then it will fail silently.
This means that if it is successful then you can expect the ID of the target shape to be equal to the ID that was set in Shape Data and if not then the opposite will be true.
The final part therefore is the User.IDsMatch
cell that carries out this check and allows you, in other formulas, to know whether you have a correct reference to the target shape.
Note, if you're wondering why
User.CandidateId
reads as "Dynamic Connector!ID()" and not "Sheet.5!ID()" it's because Visio automatically translates references into their local name format.
Let's move on to the second part of the challenge, which is to actually track a location in the target shape. For this, I'm going to focus on the orange circle.
Now that you have a method for knowing if the referenced shape is correct, you can add additional cells that you want to target by extending the trigger cell formula:
The image is too narrow so I'll expand the formula for User.TargetTrigger
:
You can see that you've got the same starting SETF
in line 1, but now there's an additional one at line 4. The job of this second SETF
is to push a POINTALONGPATH
function into User.TargetPnt
and, because that function returns a point in the local coordinate space of the connector, it is wrapped in a LOCTOLOC
function to convert it back to page coords.
The result is that User.TargetPnt
ends up with the actual point formula:
Finally you can use User.TargetPnt
from the circle's PinX
/ PinY
cells to move it to the correct position.
This allows you to either track the target postion or, manually position the shape when you don't have a 'lock' on your target. A couple of notes about this formula:
point
, Visio is able to extract the correct X/Y component when in an X/Y cell (Shape Transform, Geometry, Connections, Scratch XY etc).
SETATREFEXPR
is a function that allows incoming values from gestures in the UI to be consumed without changing the formula.
It's worth saying that I'm not advocating this technique for regular use, but for the utility/debug shape I have in mind, and for this, I think it works reasonably well.
This post is intended as a quick reference for manually editing Dynamic Connectors that I needed for a customer.
In general, Visio does a good job of handling routing using the Dynamic Connector, but occasionally, and depending on the routing style you have applied, some manual adjustment can be required.
Note that the following is based on connectors in the
Right-Angle
orStraight
state and not theCurved
state, that uses a different approach to manipulation.
A quick glossary of the terms involved:
The connector represents a single path
that is made up of a number of segments
(the above example containing two). Each segment
has a vertex
at each end, plus a single midpoint
.
Just as I finished putting the above together I realised that Chris Roth (aka Visio Guy) had got here before me and so do check out his write up on the same subject:
Also, a couple of other useful links around this topic:
In this post I'm going to look at one option for creating a table style shape that increases its row count as it is resized vertically.
I had a question the other day about how you might build a table or grid style shape that increases the number of rows it contains as it's expanded. The question included a helpful mock up of the desired behaviour:Looking at this, some initial questions I'd start off with are:
If the number of instances is a concern then you could possibly opt for generating new geometry sections rather than sub-shapes or, potentially use a Structured Diagram (SD) List type shape where adding and removing row items would be more straight forward.
It's also worth bearing in mind that a group shape can also be used as a drop target. This, in combination with another shape that acts as a drop source shape, allows you to build logically grouped shape sets. However, if you want some additional logic to handle positioning of the sub-shape, this can become tricky without code.
So the two main options that spring to mind are a single group shape + hidden sub-shape rows and a Structured Diagram - List type shape.
Given what appears to be a limited number of rows I'm going to opt for building the former option.
One of the issues that this type of shape throws up is that of how to aggregate the heights of sub-shapes, some of which you may intend to have a height of 0. You could have a formula in the group shape that directly references and sums the height of each shape, but I'm uncomfortable with the parent having too much knowledge of its children. Given that Visio's collection of ShapeSheet functions lack any kind of iteration functionality, this is probably a situation where I might break that rule.
In this case you're going to want to know where the bottom of each shape might be if it were visible. One way around this is to ensure that the rows are not of a variable height and that's the approach I'm going to take in this instance.
So let's get going and create a shape that contains 10 pre-defined rows
My plan is to have the group shape host the heading and the sub-shape, when I add them, become the row items. Before I start the construction I'll add some variables that the other components of the shape can reference.
I've just tucked that longer formula under its row, but you should be able to see the following:
Now the basic variables are in place I'll setup the heading.
You should now have a shape that looks something like this (I've add the text so you can see where it sits):
There are some options here about whether you want the border to follow the perimeter of the entire group shape rather than the heading, but I'll leave that to you to adjust if that's the way you want to go. If that is the case then I would add a second geometry section and use the first for the fill and the second for the border, setting NoFill and NoLine as appropriate.
In any case, I'm now ready to start to add the row shapes. The basic strategy here is to add the first row shape and set an index cell that will drive the position of the remaining rows.
However things are complicated a little bit by the fact that there's more than one column in the table. More choices - you could go for a single shape and then use tabs to position the two pieces of text or go for two separate shapes. The tab option doesn't handle text wrapping very nicely and so I'm tempted towards two separate shapes. This has another advantage that if some kind of conditional formatting of the value, as opposed to the label, becomes necessary you'd be able to accommodate this fairly easily.
Given that you're now looking at two shapes in a single row, you need some method to set the column widths. To do this I'm going to add a control handle just under the heading.
Right-click in the group shape ShapeSheet and add the 'Controls' section.
Rename the first row in the new section to 'Column1' and set the cell formulas as follows:
Using these formulas, you're constraining the X movement of the control to horizontal extents of the shape with the BOUND function and setting and locking the Y position to the base of the heading.
So, on to the first sub-shape - I'll just deal with the label shape as this first shape is going to act as the main template for all label and value shapes and it will hold all of the logic. The text construction and position will be driven by its row and column indices, which I'll add in a second. (Note that in the following formulas, the group shape ID is '1'.)
Add a new rectangle somewhere in the middle of the group shape, select both the group shape and your new rectangle and click Group / Add to Group.
Open the new sub-shape's ShapeSheet and add new Shape Data and User sections.
In the Shape Data section you need two rows, one named Prop.RowIdx and the other Prop.ColIdx
In the User section you need two rows as follows:
You should now have a shape that looks something like this:
It's at this stage that you should check that sub-shape functions correctly and you should play around with the Shape Data row and column indices in the sub-shape. If there are any further changes that you want to make, then this is the time to do it:
Once your happy, you can move on to locking down the the protection section.
In the group shape, add a new User cell named IsProdMode and set it to True.
Back in the sub-shape (in the Protection section) set both LockDelete and LockTextEdit to: Sheet.1!User.IsProdMode
Also in the sub-shape set the other cells to 1:
It's also difficult to know at this stage how formatting might be applied to the shape. In general, if a fill colour is applied the a group shape via the UI then all of the sub-shapes will receive that fill as well. You can prevent this from happening by setting the LockFromGroupFormat cell in each sub-shape, however this then removes a simple way of set sub-shape formatting when you do want to have this effect.
So what I'm going to do is add a right-click menu option to allow the user to toggle this cell in the sub-shape. This will allow the user to turn off the lock, apply some formatting to the whole shape, turn it back on and just change the header. It's not a perfect solution but allows for some level of flexibility.
Open the group shape's ShapeSheet
Add a User cell:
Right-click and add an Actions section, renaming the first row to Actions.LockSubs
Set the following cells:
Open the sub-shape's ShapeSheet.
Set the following cell:
Next up is the duplication phase.
Reset the sub-shape row and column index Shape Data rows to 0.
Right-click on the group shape and select Group / Open Group [This opens the group editing window (note the part of the window caption) and this allows you to work at the sub-shape level.]
Select the row item sub-shape hit Ctrl+D 19 times to duplicate the shape, and close the group editing window. (note you won't see any changes as all of the shapes will be placed in the same position)
Back in the main window click on Developer / Drawing Explorer and expand the Foreground tree node all the way down to the Shapes node of the group shape
Now, for each sub-shape select the item in the tree and then edit the row and column values to arrange all of the shape.
[Note that once there are more then a few shapes you may want to write some code to iterate through the shapes to do the same job - if that's of interest then have a look at the code at the bottom of this post, which does something similar]
With all of the sub-shapes added you can also lock down the group by setting LockGroup to 1 in the group shape.
The shape should now look something like this:
So all of the internal logic is now done so I just want to finish the shape build by constraining how the actual height of the group shape behaves.
So what's happening here?
If you've not come across the SETATREF function before, you can think of it as a method to redirect cell traffic. When the user drags the shape's height handles, Visio fires a new value into the Height cell. The SETATREF function handles this (via the SETATREFEXPR function) and forwards the value onto a cell of your choosing - in this case User.RawHeight. This in turn triggers User.VisibleRows and finally User.CalcHeight gets its turn to calculate the desired height based on a multiple of User.RowItemHeight. If you look at the third argument of the SETATREF function, you'll spot that this is '1', which tells the function to ignore the resulting value and just evaluate to 0. At the end of all of that the '+User.CalcHeight' is the resulting value that the shape will actually be set to.
If you're interested in reading more about SETATREF then I have another post here (FormulaU and FormulaForceU in Visio)
I won't step through the setting of the heading font as this is just via the UI, but once that's done the final step is to create a master shape. You can do this by dragging the shape onto the document stencil, but if you want to get more details about this process then you can checkout the 'Creating Masters and Stencils' section in 'Developing Microsoft Visio Solutions'
You can download a completed version of the above here:
The Visio Event Monitor that's part of the SDK is a great tool for understanding how Visio works and the events that are firing away while you work. So in this post I thought I'd demonstrate a quick technique for making the output a little easier to read.
By way of an example suppose you start Visio, open the Event Monitor tool and then create a new document based on the Basic Flowchart template and drop Start/End
and Decision
shapes onto the page.
After dropping the shapes you should see output in the monitor window that looks something like this:
Aside from the inability to change the zoom level, that's a lot of similarly formatted code.
What you can do is copy the contents and paste into Notepad++.
One nice feature in Notepad++ is its syntax highlighting which it will either infer from the the file extension or you can set via the Language menu.
Another aspect of the syntax highlighting is that you're also able to create a User Defined Language to define your own highlighting and, even though the event output isn't a language, you can still apply some useful rules. You can create a new language by going into Language / Define your language... This allows you to define different keywords and apply different formats for each set.
I've created a language set here, that you can import:
Once that's imported and you've pasted in the monitor output you can set the language under the Language / VisioEventMonitor menu, and the results should be something like this:
I find this very helpful. The event scopes become much clearer and there's a separation between the event name, the value and formula, and the 'more info' component of the event.
I've also grouped together the Before
(and After
) events and italised the format so they become a little clearer as well:
Anyway, I hope you find this useful and that it gives you a clearer picture of what Visio is up to.
Following on from my previous post, I thought I'd move on and look at some interesting aspects of the Congress shapes from a Visio construction perspective.
I think you can look at the building of these shapes as consisting of two stages.
The first part really requires code and although I'm going to focus this post on the ShapeSheet, broadly, I started off by laying out the shapes in bands. Each band carries a set number of shapes and from this you can calculate an equal increment in degrees and then repeat the process with an incremented radius for each band.
Once the shapes are on the page you can group the items and this takes care of setting PinX/Y cells with relative formulas and if you set the LockAspect
cell to true then none of the sub-shapes get distorted.
The first thing to do now the shapes are down is to ensure that each shape is indexed correctly as this drives seat allocation from left to right. To do this you run through the sub-shapes once more building a list of shape IDs, angle (relative to the center) and distance from the center. This list can then be sorted by angle and distance. Since you have the shape ID you can then run through the list adding a User cell (User.Idx
) with the respective index to each shape. This results in a radial layout that can be populated in a clockwise manner.
In Visio, allowing the user to be able to set formatting on a single selected shape is straight forward, but as soon as you want to extend this to multiple formatting choices things get a little more complex. For color, one option is to include a range of Shape Data rows that allow the setting of RGB's - this works but, of course, you lose the benefit of the easy UI controls.
To try and work around this I've elected to use the legend items to set formatting, by allowing each legend item to be sub-selected and then carrying the applied formatting through to the group shape. There are three parts to this technique:
For the shape text I've mapped the Shape Data row label cells to the text of the legend items and for the fill color I'm using a SETATREF
function to allow both the setting of the legend item FillForegnd cell and, to forward on that value into the group shape's User cell for the respective range.
As far as the Character
section's concerned I've chosen to make any change, applied to a Legend item, to be propagated on to all of the other Legend items. Again, this uses the SETATREF
function and any changes are pushed back to the group shape and are in turn read back by the items. This means that you can apply text formatting to either a single Legend item or, to the group shape and in each case, all of the Legend items will change together. Here's a model of how this works:
...and here's what it looks like in the ShapeSheet where you can see each cell is just mirrored back up to the same cell in the group shape:
The shape supports up to five ranges and the data for each range is set in the Shape Data
section of the group shape Prop.Range1-5
. So any given seat needs to be able to find out in which range it's sitting and then, what the color should be used for that range. The range colors are set as described above with the group shape holding five User cells: User.RangeNColor. So the final part of the puzzle is a single function cell that each seat sub-shape can call:
IF(ARG("Idx",500)<User.UpTo1,
User.Range1Color,
IF(AND(ARG("Idx",500)<User.UpTo2,Prop.RangeCount>=2),
User.Range2Color,
IF(AND(ARG("Idx",500)<User.UpTo3,Prop.RangeCount>=3),
User.Range3Color,
IF(AND(ARG("Idx",500)<User.UpTo4,Prop.RangeCount>=4),
User.Range4Color,
IF(AND(ARG("Idx",500)<User.UpTo5,Prop.RangeCount>=5),
User.Range5Color,
User.UnallocatedColor)))))
With this function cell in place the seat sub-shape can call it using the EVALCELL
function:
As I mentioned earlier, once you start to deal with a shape with more than a few sub-shapes, you will find it much easier to use code to do the work. These days I find LINQPad absolutely invaluable when working with Visio, but VBA is also very useful as it is, of course, fully baked into the product.
To make things a little simpler I also added a User.ShpType
cell to all of the sub-shapes and set the value to either "Seat" or "LegendItem". This means you can simply foreach over all of the sub-shapes targeting the type required for any changes you'd like to make:
void Main()
{
var vApp = MyExtensions.GetRunningVisio();
var grp = vApp.ActiveWindow.Selection.PrimaryItem;
const string ShpTypeCellName = "User.ShpType";
foreach (Visio.Shape s in grp.Shapes)
{
if (s.CellExistsU[ShpTypeCellName, (short)Visio.VisExistsFlags.visExistsAnywhere] != 0)
{
if (s.CellsU[ShpTypeCellName].ResultStr[""] == "Seat") //or "LegendItem"
{
//Do something to this sub-shape
}
}
}
}
Public Sub LoopCongressSubShapes()
Dim grp As Visio.Shape
Dim s As Visio.Shape
Set grp = ActiveWindow.Selection.PrimaryItem
Const ShpTypeCellName As String = "User.ShpType"
For Each s In grp.Shapes
If s.CellExistsU(ShpTypeCellName, Visio.VisExistsFlags.visExistsAnywhere) <> 0 Then
If s.CellsU(ShpTypeCellName).ResultStr("") = "Seat" Then 'or "LegendItem"
'Do something to this sub-shape
End If
End If
Next
End Sub
Of course, as with many things in Visio, there's no one answer to solving these problems, but I hope that above helps you think about your own Visio shape building design choices.