One of the most common, most basic, and most mindless, things we do with computers every day is open windows. Launching a program or opening a document is often synonymous (on a practical level at least) with opening a window. As common as this action is, we rarely give any thought to what is going on behind the scenes when we open a window – hence the wisecrack about it being a mindless operation.
However, if we want to make the most of our design efforts, be need to replace this “mindlessness” with “mindfulness” by really thinking about the things that make windows easy and comfortable to use.
Defining a Well Behaved Window
As we begin looking at the behavior of windows, I want to emphasise that I am not talking about user interface design. User interface design deals with the details of what a window does functionally. Rather what I’m talking about is an examination to the behaviors a window should exhibit, regardless of what happens to be on the screen.
If we think about the window as a kind of frame that supports the interface’s core functionality, we see that one of the big things a window can do is remember things. Over the years, people have developed, and posted on the LabVIEW forums, a variety of toolboxes for storing generic window information like screen customizations, positions and settings. One of these toolsets combined with an event-driven structure can make it easy to significantly pump up the convenience factor of just about any application.
To see how these sorts of tools work, we’re going to enhance our undockable windows application with a simple addition that automatically saves a windows last position and restores that position the next time the window undocks. Although the basic logic is simple, it provides us with the opportunity to discuss many of the major issues that impact this sort of functionality.
The Data, and Where to Keep it
When considering the data that this sort of functionality requires and uses, the operative word is: “convenience”. By that I mean that this data may make using the screen more convenient, but nobody is going to be crying if it gets lost. In fact, a valuable behavior is the ability basically “reset” all the stored data back to its default value by simply deleting the data from the file that is storing it and letting the application rebuild it as needed.
Likewise the data should be of low “intelligence” value. In other words, we don’t want to include things in this data that could constitute a security risk. However, having said that, we also want to make sure that a well-meaning user doesn’t mess up the program’s operation by manually editing the data. My approach to blocking such edits has three major points:
- Be careful about what you name things: You want to give identifiers that are, of course, meaningful. However, you don’t want to use names that will call attention to themselves in a way that says, “Hi, I’m a setting you might want to play with…”
- Use a non-obvious data structure: For example, in our example we don’t save a window’s position as a simple list of four values. The problem is that a user looking at these values might decide to try to edit them manually – a simple act that could have some significant side effects. To see why consider that the way you move a window around the screen is by changing the VI’s
WinBoundsproperty. However, this property defines a windows position by essentially specifying the location of the window’s upper-left and lower-right corners. Consequently, while it does set the window’s location, it is also specifying the window’s size.
- Provide a simple way to validate the data: Given that there is no way to know ahead of time what sort of data you might be wanting to store, validation might seem like a huge task, but it’s really not. Remember, when validating the data you don’t have to prove the data values are valid, just that they haven’t changed since the program wrote them.
As we get into how the position saving is implemented, you’ll see how I put these ideas into action, but first we need to look at how we are going to implement the capability from a high-level view.
Our Basic Approach
When adding in new or enhanced functionality, you want to do so in a way that requires as few modifications to (and has as little impact on) the existing structure, as possible. This ability to easily incorporate new functionality is a large part of the meaning of the term, “maintainable”. It is also why it is always good to think about your overall application in terms of functional blocks – or specific VIs that do specific things, and handle specific situations.
With that point in mind we know we have two basic operations we need to add: one sets the position when we open a VI and one writes a new position when we close it. Of these two operations, the simplest is the one that reads the last saved location and moves the window to that location. It’s simple because there is only one place in the code where that opening takes place, and that is right here:
This is the VI
Float the VI.vi and if you compare it to the version that I presented last week you will note that it has one extra subVI that uses a reference to the VI being opened to look up and set the window position. we’ll look at exactly how it does that in just a moment. The operation that saves a VI’s last open position can also be boiled down to a single subVI, but due to the nature of our application, it will have to be installed in two locations.
Here’s the first of those locations. It occurs in the subVI
Unfloat the VI.vi and it handles the case where the user closes any of the floating windows. Again you’ll notice one added subVI. Using the VI reference supplied to it, the subVI determines the target VI’s new window position and saves it. The other place where this VI occurs is in the event that stops the application.
Here the event logic checks to see if a window is docked, and if not calls the same subVI to save the window position of the VI associated with the reference.
Digging for Details
Now that we see where the modification fits into the application, let’s look at how the subVIs work – starting with the routine that saves the window position.
As you can see, I am using the configuration file VIs to store the data in a text file using the INI file structure. However, it’s not the application’s INI file but rather one that I am creating in the user’s “My Documents” directory. This selection has at least a couple of implications. First it means that every user that logs into the computer will have their own set of customizations. Second, if the user wants to reset all their customizations back to default, all they have to do is delete or rename that one file.
Next, notice that this VI was designed to be usable in two different ways. If the VI reference input carries a valid reference the code uses that reference to get the data it wants to save. Alternatively, if the VI reference provided is not valid, the true case of the structure (not shown) open a reference to the VI calling this subVI and save the data for it.
Finally, let’s look at the subVI that the code uses to convert the window bounds data into the string that will be saved to the custom INI file.
In keeping with the concepts I cited earlier, I obfuscate the datatype by flattening the structure to a string, convert the string into an array of U8s, and finally format the array as a string of 2-character hexadecimal values. However, before making that last conversion I provide a mechanism for ensuring data validity: I append a 16 bit CRC. The result is a string that will allow you to detect if it has been manipulated outside the program.
Turning now to the VI that retrieves the data, we see logic that reverses what was done in the position save routine. However, there is one added twist: if this VI is being called for the first time, the position of the target VI might not be in the INI file. Consequently the code needs to be able to recognize that situation and just let the windows open in its default position.
The subVI (below) that converts the string from the INI file back into the LabVIEW data structure for defining a windows bounds, generates a structure containing all 0s when it is passed an empty string – which is what you get when you try to read a string value from an INI file that doesn’t exist.
Also notice how this VI verifies the CRC. It’s commonly believed that in order to verify a CRC you have to split the CRC from the message, calculate a new CRC on the message and compare the result to the CRC received as part of the message. However, such is not the case. Due to the way the CRC calculation works, if you simply perform a CRC on the entire message including the original CRC the results will always be 0 for a valid message and CRC. Hence, the logic only goes to the trouble of splitting the message from the CRC after it has determined that the message is valid. In the case where the second CRC calculation is not zero (indicating a corrupted message) the logic outputs the same invalid data structure that you get from a null input string.
Given these facts, the VI for reading the target VI’s last position only has to look for that known-invalid data structure and if it finds it, bypasses the logic that set the window’s bounds.
The Big Tease
So there we have it, a basic framework that you can use to implement a variety of “window memory” functions. But what about next time? I have talked a lot about processes that run in the background. What happens though, if you want to be able to provide a minimal interface that isn’t always there but can be easily called up when needed. Next time I’m going how you can utilize the Windows system tray to house just such an interface. At the same time we’ll look at one of the more interesting backwaters of LabVIEW development – .net callbacks.
Until Next Time…