AppleScript to toggle geek tools picture on and off - applescript

relatively new to coding - I do some music related things and I'm trying to create an overlay to alert me when certain filters are active
geek tools can display a picture which works fine, I can make it appear and disappear, I'm just trying to toggle it on and off
I've got:
tell application "GeekTool Helper"
set g to geeklet id "C50F8977-88DF-4813-82C3-23D1C23F4624"
set visible of g to true
end tell
and then the same thing as "false" to make it go away. Again just trying to toggle it back and forth for on one key. I'm sure this is super easy - would really appreciate it if anyone has a sec to help out. Guessing it's some sort of if/then or something...
thank you

I'm not familiar with this application, but I'm going to assume that your code snippet exemplifies a fully-working script that displays the referenced picture:
tell application "GeekTool Helper"
set g to geeklet id "C50F8977-88DF-4813-82C3-23D1C23F4624"
set visible of g to true
end tell
In order to toggle the state of the picture's visibility, simply apply standard boolean logic. In case you're not familiar with this, it's basically the notion that true and false are mutually-exclusive (i.e. something cannot be both true and false), and they are each the other's inverse (i.e. something that is not true must therefore be false, and something that is not false must therefore be true).
In many programming and scripting languages, the boolean values of true and false are commonly represented by constants of the same name. In AppleScript, as you already know, these constants are true and false. There are a trio of boolean operators, and, or, and not. The last one of these operators is the key to implementing a state toggle, on the basis that:
not true = false
and:
not false = true
Borrowing the references from your script, toggling works on the underlying principle of evaluating the property to discern its present state, and then assigning a new value to reflect the opposing state. Here's the logic laid out in full:
if the visible of g = true then
set the visible of g to false
else if the visible of g = false then
set the visible of g to true
end
This is one instance where AppleScript's "natural language" paradigm does precisely what it says, so this is hopefully self-explanatory. (This is not always going to be be the case, just to warn you.)
While this is perfectly ready to cut and paste into your script, there's a few things we can do to clean it up a bit, one of which I alluded to earlier, namely using the not operator to reduce some of the repetitive exposition of logic that can become tiresome.
not is acts upon a single boolean value, and gives us back the opposite (or negated) value, e.g:
not true
Result: false
Applying this to one half of the logic from previously, this:
if the visible of g = true then set the visible of g to false
is equivalent to this:
if the visible of g = true then set the visible of g to not true
Likewise, the same applies to the second half of the logic in an identical fashion:
if the visible of g = false then set the visible of g to not false
The not operator needn't only be applied to the explicit true and false constants. After all, if this test is passed:
if the visible of g = true
this implies that visible of g—which is equal to true—is, therefore, itself a boolean value. We can thus apply the not operator directly to the property itself:
if the visible of g = true then set the visible of g to not visible of g
Likewise, for the opposing start state:
if the visible of g = false then set the visible of g to not visible of g
Thus, what initially seemed like two separate conditions being evaluated, and two distinct outcomes being catered for by our code, is, in fact, functionally a singular process that acts upon the state in an identical fashion irrespective of what the initial value of the state is.
What this means is that we need never have to know the starting state in order to effect a toggle, and therefore, we don't actually have to test it at all. We can simply do this:
set the visible of g to not visible of g
Inserting this back into you original script, we get this toggling capability:
tell application "GeekTool Helper"
set g to geeklet id "C50F8977-88DF-4813-82C3-23D1C23F4624"
set visible of g to not visible of g
end tell
Whilst we're done with respect to implementing the logic, we can also clean up the references to the visible property a little bit in much the same way as we did with the boolean expressions above. From hereon, the principles will be much more specific to AppleScript, although it's by no means the only language which defines a clear ownership hierarchy for the various elements that appear in a script. For your script, we begin at the very top-level that isn't immediately obvious, but it always present: that's the AppleScript instance itself, which is represented by the enumerated value assigned to the constant current application. Everything else in a script is ultimately owned by the current application.
Moving down a level, one of the current application's immediate descendants is the application class object for the GeekTool Helper app. This is where relationships and ownership becomes more overt in the code, as AppleScript provides the keyword tell in order for us to access the child objects belonging to an element. So moving down another level, we encounter the geeklet class object, which is the direct descendant of the GeekTool Helper application object. Finally, in our case, this brings us to one of likely many properties belonging to any geeklet class object, in this case, the visible property that reports on as well as dictates back the visibility of the picture referenced by the geeklet element.
The point of this hierarchy is exemplified in the way you've enclosed the progeny of an application class object within a singular tell block, which forwards any statements that arise to the application element without the need to continuously reference the parent object every time we wish to access one of its children. It's not so obvious how convenient this ends up being because your script is very short. Nonetheless, I'm going to milk it for all its worth, and I'll start by removing the declaration of the variable g and, instead, tell-ing the geeklet element what to do:
tell application "GeekTool Helper"
tell geeklet id "C50F8977-88DF-4813-82C3-23D1C23F4624"
set visible to not visible
end tell
end tell
Since this new tell block creates an enclosure similar to the one it itself lives in, commands will get forwarded onto the geeklet without having to be explicit about referring to it each time. This is why we no longer require to say visible of g (or visible of geeklet id "..."), and we can just focus on the property that we intend to manipulate.
As each of the tell blocks only contains a single line or a single block, these can be collapsed into a single-line, compound tell statement by way of the to joiner:
tell application "GeekTool Helper" to tell geeklet id ¬
"C50F8977-88DF-4813-82C3-23D1C23F4624" to set ¬
visible to not visible
It still looks like three lines, but it's one line divided over three lines mostly for readability. The continuation character (¬) lets AppleScript know to continue reading the next line as it's part of the same instruction.
For the final step, let's put this into a handler, which will allow this toggling functionality to become a generalised tool that can be used to toggle any geeklet whose id value is given to the handler:
to toggle(uuid)
tell application "GeekTool Helper" to tell ¬
geeklet id uuid to set visible to ¬
not visible
end toggle
This can appear anywhere in your script, although it's sensible to put it (and any other handlers you create yourself in the future) at the bottom of the script, together in one place. To make us of it, you simply call it with the id value of the geeklet object to be toggled:
toggle("C50F8977-88DF-4813-82C3-23D1C23F4624")
FIN ◻︎
toggle("C50F8977-88DF-4813-82C3-23D1C23F4624")
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
to toggle(uuid)
tell application "GeekTool Helper" to tell ¬
geeklet id uuid to set visible to ¬
not visible
end toggle

Related

In macOS AppleScript, how do I check if a property exists?

In AppleScript I access an object in an application, e.g.:
tell application id "DNtp"
repeat with g in (children of root of (think window 1))
set theAnnotation to annotation of g
end
end tell
I know that depending on the child accessed, g sometimes has no annotation property.
N.B. This is not the same as having a missing value or something like that.
When running the code, AppleScript simply complains that the variable theAnnotation is not defined.
Other than trapping the error with a try ... on error statement, how do I check if g has the property annotation?
If you do
tell application id "DNtp"
annotation of children of root of (think window 1)
end tell
You'll get a list containing annotations and if those are missing, missing values, like:
--{missing value, missing value, missing value, «class DTcn» id 49 of «class DTkb» id 1, missing value}
The problem you describe in DEVONthink (of AS ignoring a declared variable during compile time) is a problem I've seen in other apps, too (but it's fairly rare).
If you want to check for the existence of a property, what usually works (and I've tested this with DEVONthink3) is to use exists, like:
if (exists annotation of g) then
Which will return true or false as you would expect. Not sure how you'd use that in the way you first posted, but I don't really know all the steps you're taking, so ....
I hope this helps
I don't use DEVON-related software, but in ordinary1 situations when dealing with AppleScript records, CRGreen's suggestions won't apply: exists is not a command that properties understand, especially non-existent properties; and referencing a property that does not exist will throw an error.
I'm glad you're looking for an alternative to try...end try. I've seen your previous code samples that are drowning in them, and they are expensive operations when an error is caught, so not ideal for what you're attempting. try really has no place in AppleScript at all.
Rather than testing for the existence of a property within a record, the way to approach this in a general context is to create a record object that contains all the property values that would ever be needed, and to assign default values to each of them.
In AppleScript, a record is observed to follow the following behaviour:
A single record object can only contain one property with a given identifier. Should you attempt to insert two properties both of which are identified the same, the compiler will keep the first property and its associated value, and scrub the rest:
{a:1, b:2, a:3} # will resolve on compilation immediately to {a:1, b:2}
Two record objects can contain properties with shared identifiers, like so:
set L to {a:1, b:2, c:3}
set R to {d:missing value, c:L}
Similarly to list objects, two record objects can be merged into a single record, and the properties will be amalgamated: properties with identifiers unique to each record will simply be inserted without any change into the resulting data structure. Where an identifier occurs in both record objects before the merge, again, precedence is given in a left-to-right reading order, so the properties in the prefixed record (on the left) will prevail and the suffixed record (on the right) will have its non-unique property identifiers (and their values) scrubbed:
L & R --> {a:1, b:2, c:3, d:missing value}
R & L --> {d:missing value, c:{a:1, b:2, c:3}, a:1, b:2}
Your code snippet contains this:
repeat with g in (children of root of (think window 1))
set theAnnotation to annotation of g
end
Therefore, g is an item contained within children (a list object), and the type class of g is a record. Depending on which item of children is being examined, I'm assuming that some of those items are records that do contain a property identified by annotation, and some of those items are records that do not contain such a property.
However, consider the following record that results from this merge:
g & {annotation:missing value}
Here are the two possible scenarios:
g is a record that already contains a property identified as annotation, e.g.:
set g to {cannotation:"doe", bannotation:"ray", annotation:me}
g & {annotation:missing value} --> {cannotation:"doe", bannotation:"ray", annotation:«script»}
set theAnnotation to annotation of (g & {annotation:missing value})
--> «script» (i.e. me)
OR:
g is a record that in which the property identifier annotation does not exist, e.g.:
set g to {doe:"a deer", ray:"a drop of golden sun"}
g & {annotation:missing value} --> {doe:"a deer", ray:"a drop of golden sun", annotation:missing value}
set theAnnotation to annotation of (g & {annotation:missing value})
--> missing value
Therefore, for every place in your scripts where try...end try has been used to catch non-occurrences of properties inside a record data structure, simply delete the try blocks, and wherever you are assign values read from speculative property values, artificially insert default values you can then test for and will know whether or not the value came from your DEVONthink source or from your brain:
tell application id "DNtp"
repeat with g in (children of root of (think window 1))
set theAnnotation to annotation of (g & {annotation:false})
if theAnnotation ≠ false then exit repeat
end
end tell
1This isn't in any way meant to suggest his solution isn't viable. If DEVON returns collections that are not de-referenced--and it very well may do--these can be operated upon as a whole, without looping through individual items, and he, of course, uses DEVON. But the situation I hopefully address above is one that arises much more commonly, and will also work here.

UFT/QTP - Extract Values From List Within WebEdit

I am attempting to capture all the list items in the WebList elements throughout the entire application, however, while below code works on the WebLists, it does not work on this WebEdit.
When you click on the WebEdit, a long list of values appear (similar to a WebList) and as you type for your value, the list becomes shorter. That is how the WebEdit was set up.
But now, how do I get the values in this list?
Here is the code I have for the WebLists:
Code
Set WebLink = Browser("browser").Page("page")
listval = WebLink.WebElement("xpath:= ((//*[contains(text(), 'Name')]))[1]/following::SELECT[1]").GetROProperty("all items")
listvalues = split(listval,";")
For j = LBound(listvalues,1) To UBound(listvalues,1)
'Print listvalues(j)
writeToTextFile(listvalues(j))
Next
ExitTest
The short answer is: it depends on the implementation.
The long one:
There is no universal widget for comboboxes (Like there is for edit fields or lists / selects, radiobuttons etc) => there is no universal solution but only guidelines.
You need to spy on those objects that appear in the combobox, see their XPath and / or other properties (the css classname they belong to, for example) and then execute a second query that selects all such items. Afterwards you have to extract the value of the selected elements; which might be as simple as getting the innertext Property or you may need to dig even deeper in the HTML hierarchies.
You would need to pay careful attention for synchronisation(Waiting until all search result elements appear), Filtering (using the XPath, Description Objects and ChildObjects method on your WebPage) and then extraction( getting the property /element that contains the actual value of that WebElement)
So again: These combobox solutions are not universal therefore without seeing their code the best what one can provide to you is universal guidelines which should work in most of the situations. (You would need some familiarity with Web Programming and the UFT Framework / Robot)

Interpret "Find action" results on Safari

I am trying to create an AppleScript that can find text on a webpage and tell me the amount of matches I received(Command + F).
I already know how to do the "Find" part:
tell application "System Events"
delay 0.5
keystroke "f" using {command down}
end tell
However, I do not know how to interpret these results, such as tell me whether there is a match, or how many matches I have.
Is there any way to do this?(If it seems a bit vague, I can be more specific)
Thanks!
I agree with #user3439894 and his sentiments about using UI scripting (that is—in this case—getting System Events to issue mouse clicks and keypresses on your behalf). Although it has its uses in other areas, it's by far and away my personal least favourite method to achieve a goal, and one of last resort.
Two very quick reasons why it can be a fragile implementation is: 1) the CmdF shortcut used to initiate the Find... menu command could change, either by your own doing, or if it were to be overridden by a systemwide shortcut that supersedes Safari's claim to it (in fact, for this reason, I would personally trigger the Find... command via the menu bar, which System Events can click on your behalf. Menu items tend not to change like shortcuts, unless regional language settings do); and 2) if Safari loses focus during the time the keypresses are issued and the search is initiated, it messes up the whole command flow in your script, and will at best give you no results, but more likely, throw an error in a later part of the script.
I'm going to demonstrate two alternative methods of searching a Safari webpage for a piece of text, and obtaining the number of times it matches throughout the whole document.
1. Safari's do JavaScript AppleScript command
Most modern web browsers have the ability to run JavaScript code inside their tabs, and Safari can too. AppleScript can take command of this very useful function, provided you give it permission to do so by going into the Develop menu of Safari and ticking Allow JavaScript from Apple Events and Allow Remote Automation (the latter of which will already be on, I'm guessing). There's another menu item called Allow JavaScript from Smart Search Field—I would advise you keep this one off, otherwise it could potentially allow naughty websites to issue commands to your computer and cause mischief.
use A : application "Safari"
set D to the front document of A
set s to "AppleScript" -- the search string
try
set n to (do JavaScript ¬
"document" & ¬
".body" & ¬
".innerText" & ¬
".match(/" & s & "/ig)" & ¬
".length;" in D) as integer -- the number of matches
on error
set n to 0
end try
To break this down: s is the search string that you would otherwise be typing into the search box. It is fed into a JavaScript command, that has the following components:
document: a reference to the Safari webpage document;
body: the body of the document, as opposed to, say, the header or the footer. It's the main bulk of the webpage that users see in front of them;
innerText: the text contained within the body of the document, free of any HTML formatting, but preserving whitespace;
match(): a method or function in JavaScript where the search string s is used to perform a search within the innerText and return an array listing all of the matches;
length: a property of the array returned by match() that reports how many elements is contains, and this equates to the number of matches found during the search.
It's all one command, which, written in full on a single line, looks like this (using the search string "AppleScript"):
document.body.innerText.match(/AppleScript/ig).length;
It returns a number, which is stored in the variable n, and that's it.
This is my favourite method that I would elect to use myself, as it's unlikely to break, and it's nice and fast.
I should point out that match() actually searches and matches using a Regular Expression. I won't go into them right now, but it means that the search string s will need to be a little careful if using any special characters:
\ [ ] { } ^ $ . | ? * + ( )
All you need to be aware of is that, if your search string uses any of these characters, you should precede it with a double-backslash \\. So, for example, if I wanted to search for "matches I received(Command + F)" (which uses (, ) and +), then I would declare my variable s as:
set s to "matches I received\\(Command \\+ F\\)"
2. Chop & Measure
This method is useful if you don't wish to enable Remote JavaScript in your browser, or simply want something that's straightforward to remember and implement off the top of your head next time.
It's simple text manipulation, using AppleScript's text item delimiters and a bit of counting:
use A : application "Safari"
set D to the front document of A
set s to "AppleScript" -- the search string
set T to text of D -- the webpage text content
set l to {initialValue:length of T, finalValue:missing value}
set the text item delimiters to s
set T to text items of T
set the text item delimiters to ""
set T to T as text
set l's finalValue to length of T
set |𝚫l| to (l's initialValue) - (l's finalValue)
set n to |𝚫l| / (length of s)
Safari has a useful AppleScript property called text, which refers to the text content of the specified document or tab (it also has another property called source that contains the HTML source of the document or tab).
Here's the breakdown:
The value of Safari's text property—which is the text content of the webpage—is stored in a variable, T;
The length of T is read and stored. This equates to the number of characters on the whole webpage;
The text item delimiters are set to the search string, s, (which does not need to worry about special characters, so don't insert unnecessary backslashes in this one). The text item delimiters basically erase all occurrences of s from within T;
Then the length of T is read again. If s found any matches in T, it means that the length of T—the number of characters—will have reduced;
It will have reduced by the number of characters in s for each match that occurred. Therefore, turning the equation round a bit, the number of matches, n, is equal to the change in length of T divided by the length of s.
There are other ways to search a webpage with AppleScript, JavaScript, bash, etc., but I think these two serve as reasonable examples of how to achieve the same goal using very different methods. I refer to them as examples, because you might need to make small adjustments to the script to cater for your own needs, such as inserting backslashes where necessary in the first example, or considering in the second how you'd handle the situation if you set s to be an empty string "" (it will throw an error, but this is easily managed).
They also both return real values for n, i.e. 11.0. It's easy to see why in the second example, but I assume it's just a type conversion between JavaScript and AppleScript in the first example (I don't know). Therefore, purely for neatness, I would then coerce the returned value into an integer as I did in the first one, so it reads 11 instead of 11.0:
set n to (...) as integer
but you don't have to.
First of all I must say that UI Scripting can be messy and unreliable. I'd suggest you find a different way to accomplish whatever the real goal is.
That said, using Safari in macOS High Sierra 10.13.3 set to this web page, the following example AppleScript code will set the variable theSearchResult to the result of the search for the word "vague":
tell application "Safari" to activate
delay 0.5
tell application "System Events"
keystroke "f" using command down
delay 0.2
keystroke "vague"
delay 0.2
set theSearchResult to (value of static text 1 of group 2 of tab group 1 of splitter group 1 of window 1 of application process "Safari")
delay 0.2
key code 53 -- # Esc
end tell
return theSearchResult
Result:
"1 match"
Note that the value of the delay commands may need to be adjusted for your system, and or additional delay commands may or may not be needed. Adjust values of and or add/remove the delay commands as appropriate.
The search result can be one of the following, Not found or an integer followed by the word match, e.g. 1 match, and possibly something else, not sure as I've not done extensive testing.
How you want to interpret the result is up to you. You could use a if statement on the theSearchResult, e.g.:
if theSearchResult contains "Not found" then
-- # Do something.
-- # Your code goes here.
else
-- # Do something else.
-- # Your code goes here
end if
Another factor to consider is how is it being searched, i.e. Starts With or Contains. I believe the default in for Safari in macOS High Sierra 10.13.3 is Starts With.
Note: The example AppleScript code is just that and does not employ any error handling and is meant only to show one of many ways to accomplish a task. The onus is always upon the User to add/use appropriate error handling as needed/wanted.

VB6, Adding an integer to a control name in a for loop

I am currently trying you learn VB6 and came across this issue.
I wanted to loop through a for loop and adding a number to a control name.
Dim I As Integer
For I = 1 To 5
S = CStr(I)
If TextS.Text = "" Then
LabelS.ForeColor = &HFF&
Else
LabelS.ForeColor = &H80000012
End If
Next I
This S needs to be added to Text and Label so the colour will be changed without needing to use 5 If Else statements
I hope you can help me with this.
From your comment below:
What i mean is this: If Text1.text = "" Then I need this 1 to be replaced with the variable I, so the for loop can loop through my 5 textboxes and the same for my Labels.
You can't do that (look up a variable using an expression to create its name) in VB6. (Edit: While that statement is true, it's not true that you can't look up form controls using a name from an expression. See "alternative" below.)
What you can do is make an array of your textboxes, and then index into that array. The dev env even helps you do that: Open your form in the dev env and click the first textbox. Change its name to the name you want the array to have (perhaps TextBoxes). Then click the next textbox and change its name to the same thing (TextBoxes). The dev env will ask you:
(Don't ask me why I have a VM lying around with VB6 on it...)
Click Yes, and then you can rename your other textboxes TextBoxes to add them to the array. Then do the same for your labels.
Then your code should look like this:
For I = TextBoxes.LBound To TextBoxes.UBound
If TextBoxes(I).Text = "" Then
Labels(I).ForeColor = &HFF&
Else
Labels(I).ForeColor = &H80000012
End If
Next
LBound is the lowest index of the control array, UBound is the highest. (You can't use the standard LBound and Ubound that take the array as an argument, because control arrays aren't quite normal arrays.) Note also that there's no need to put I on the Next line, that hasn't been required since VB4 or VB5. You can, though, if you like being explicit.
Just make sure that you have exactly the same number of TextBoxes as Labels. Alternately, you could create a user control that consisted of a label and a textbox, and then have a control array of your user control.
Alternative: : You can use the Controls array to look up a control using a name resulting from an expression, like this:
For I = 1 To 5
If Me.Controls("Text" & I).Text = "" Then
Me.Controls("Label" & I).ForeColor = &HFF&
Else
Me.Controls("Label" & I).ForeColor = &H80000012
End If
Next
This has the advantage of mapping over to a very similar construct in VB.Net, should you migrate at some point.
Side note:
I am currently trying you learn VB6...
(tl;dr - I'd recommend learning something else instead, VB6 is outdated and the dev env hasn't been supported in years.)
VB6's development environment has been discontinued and unsupported for years (since 2008). The runtime is still (I believe) supported because of the sheer number of apps that use it, although the most recent patch seems to be from 2012. But FWIW, you'd get a better return on your study time learning VB.net or C#.Net (or any of several non-Microsoft languages), rather than VB6...

What's so bad about using button captions as variables in VB6?

I received some justified critical feedback on my last question (How to gracefully exit from the middle of a nested subroutine when user cancels?) for using the caption of a command button as a state variable. I did it because it's efficient, serving two or three purposes at once with very little code, but I understand how it could also cause problems, particularly in the slightly sloppy way I originally presented it.
I feel like this deserves its own discussion, so here's the same idea cleaned up a bit and modified to do it "right" (which basically means defining the strings in a single place so your code won't start failing because you simply changed the text of a command button). I know my variable and control naming convention is poor (OK, nonexistent), so apologies in advance. But I'd like to stay focused on the caption as state variable discussion.
So here we go:
' Global variables for this form
Dim DoTheThingCaption(1) As String
Dim UserCancel, FunctionCompleted As Boolean
Private Sub Form_Initialize()
' Define the possible captions (is there a #define equivalent for strings?)
DoTheThingCaption(0) = "Click to Start Doing the Thing"
DoTheThingCaption(1) = "Click to Stop Doing the Thing"
' Set the caption state when form initializes
DoTheThing.Caption = DoTheThingCaption(0)
End Sub
Private Sub DoTheThing_Click() ' Command Button
If DoTheThing.Caption = DoTheThingCaption(0) Then
UserCancel = False ' this is the first time we've entered this sub
Else ' We've re-entered this routine (user clicked on button again
' while this routine was already running), so we want to abort
UserCancel = True ' Set this so we'll see it when we exit this re-entry
DoTheThing.Enabled = False 'Prevent additional clicks
Exit Sub
End If
' Indicate that we're now Doing the Thing and how to cancel
DoTheThing.Caption = DoTheThingCaption(1)
For i = 0 To ReallyBigNumber
Call DoSomethingSomewhatTimeConsuming
If UserCancel = True Then Exit For ' Exit For Loop if requested
DoEvents ' Allows program to see GUI events
Next
' We've either finished or been canceled, either way
' we want to change caption back
DoTheThing.Caption = DoTheThingCaption(0)
If UserCancel = True Then GoTo Cleanup
'If we get to here we've finished successfully
FunctionCompleted = True
Exit Sub '******* We exit sub here if we didn't get canceled *******
Cleanup:
'We can only get to here if user canceled before function completed
FunctionCompleted = False
UserCancel = False ' clear this so we can reenter later
DoTheThing.Enabled = True 'Prevent additional clicks
End Sub '******* We exit sub here if we did get canceled *******
So there it is. Is there still anything really that bad about doing it this way? Is it just a style issue? Is there something else that would give me these four things in a more desirable or maintainable way?
Instant GUI feedback that user's button press has resulted in action
Instant GUI feedback in the location where user's eyes already are on how to CANCEL if action is not desired
A one-button way for users to start/cancel an operation (reducing the amount of clutter on the GUI)
A simple, immediate command button disable to prevent multiple close requests
I can see one concern might be the close coupling (in several ways) between the code and the GUI, so I could see how that could get to be a big problem for large projects (or at least large GUIs). This happens to be a smaller project where there are only 2 or 3 buttons that would receive this sort of "treatment".
The single biggest problem with this technique is that it uses a string as a boolean. By definition, a boolean variable can have only two states, while a string can have any number of states.
Now, you've mitigated the danger inherent in this somewhat by relying on an array of predefined strings to define allowed values for the command button text. This leaves a handful of lesser issues:
Logic is less-than-explicit regarding current and available states (there are actually four possible states for the form: not-started, started, completed, started-but-canceling) - maintenance will require careful observation of the potential interactions between button text and boolean variable states to determine what the current state is / should be. A single enumeration would make these states explicit, making the code easier to read and understand, thereby simplifying maintenance.
You're relying on the behavior of a control property (button text) to remain consistent with that of the exposed property value type (string). This is the sort of assumption that makes migrating old VB6 code to newer languages / platforms absolute hell.
String comparison is much, much slower than a simple test of a boolean variable. In this instance, this won't matter. In general, it's just as easy to avoid it.
You're using DoEvents to simulate multi-threading (not directly relevant to the question... but, ugh).
The biggest issue i've come accross when working on (very old) code like this [button captions as variables] is that globalisation is a nightmare.... I had to move a old vb6 app to use English and German... it took weeks, if not months.
You're using goto's as well..... a bit of refactoring needed perhaps to make the code readable??
**Edit in response to comments
I'd only use a goto in vb6 at the top of each proc;
on error goto myErrorHandler.
then at the very bottom of the proc i'd have a one liner that would pass err to a global handler, to log the error.
Ignoring the general architecture/coupling problems because you are aware of those issues, one problem with your approach is because VB6 controls do magic stuff when you set properties.
You may think you are just setting a property but in many cases you are causing events to fire also. Setting a checkbox value to true fires the click event. Setting the tabindex on a tab control causes a click event. There are many cases.
If I remember correctly I also think there are scenarios where if you set a property, and then read it immediately, you will not see the update. I believe a screen refresh has to occur before you see the new value.
I have seen too much horrible VB6 code that uses control properties as storage. If you ever find this kind of code you will recognize it because it is scattered with redundant calls to Refresh methods, DoEvents and you will frequently see the UI get hung. This is often caused by infinite loops where a property is set, an event is fired and then another property is set and eventually someone writes a line of code that updates the first property again.
If those issues don't scare you enough then think of this. Some of us just are not that smart. I've been coding in VB6 for over 10 years and have personally written probably around 750K LOC and I keep staring at your example above and I find it very difficult to understand what it going on. Assume that all the people that will need to read your code in the future will be really dumb and make us happy by writing really simple looking code.
I think it's better to decouple the caption text from the state of processing. Also the goto's make it hard to read. Here is my refactored version...
Private Const Caption_Start As String = "Click to Start Doing the Thing"
Private Const Caption_Stop As String = "Click to Stop Doing the Thing"
Private Enum eStates
State_Initialized
State_Running
State_Canceled
State_Completed
End Enum
Private Current_State As eStates
Private Sub Form_Initialize()
DoTheThing.Caption = Caption_Start
Current_State = State_Initialized
End Sub
Private Sub DoTheThing_Click()
If Current_State = State_Running Then
'currently running - so set state to canceled, reset caption'
'and disable button until loop can respond to the cancel'
Current_State = State_Canceled
DoTheThing.Caption = Caption_Start
DoTheThing.Enabled = False
Else
'not running - so set state and caption'
Current_State = State_Running
DoTheThing.Caption = Caption_Stop
'do the work'
For i = 0 To ReallyBigNumber
Call DoSomethingSomewhatTimeConsuming
'at intervals check the state for cancel'
If Current_State = State_Canceled Then
're-enable button and bail out of the loop'
DoTheThing.Enabled = True
Exit For
End If
DoEvents
Next
'did we make it to the end without being canceled?'
If Current_State <> State_Canceled Then
Current_State = State_Completed
DoTheThing.Caption = Caption_Start
End If
End If
End Sub
Apart from removing the GOTos as DJ did in his answer, there is nothing really wrong about your approach. The button caption can have only two states, and you use those two states to define the flow in your code.
I have however two reasons why I would do it differently:
Your method creates problems when you want to translate your program into a different language (in my experience you should always plan for that), because the captions would change in another language
It goes against the principle of seperating the user interface from the program flow. This may not be an important thing for you, but when a program gets bigger and more complex, having a clear seperation of the UI from the logic makes things much easier.
To sum it up, for the case at hand your solution certainly works, and there is no reason why it shouldn't. But on the other hand experience has taught us that with more complex programs, this way can cause problems which you can easily avoid by using a slightly different approach.
Also, I think it is safe to assume that everybody who criticised your example did so because they made a simnilar choice at some point, and later realised that it was a mistake.
I know I did.
This ties your underlying algorithm to specific behavior in your UI. Now, if you want to change either one of them, you have to make changes to both. As your app grows in size, if you don't keep your changes local by encapsulating logic, maintenance will become a nightmare.
If anyone for any reason ever needs to work on your code, they won't find practices and conventions they are familiar and comfortable with, so the boundaries of functionality won't exist. In other words, you are headed in the wrong direction on the Coupling/Cohesion trail. Functionally integrating State management with the UI is the classic poster child for this issue.
Do you understand OOP at all? (Not a criticism, but a legitimate question. If you did, this would be a lot clearer to you. Even if it's only VB6 OOP.)
Localization has the biggest impact on the type of logic OP is presenting. As several people mentioned it - what if you need to translate the app into Chinese? And German? And Russian?
You'd have to add additional constants covering those languages too... pure hell. GUI data should remain what it is, a GUI data.
The method OP describes here reminded me what Henry ford said: "Any customer can have a car painted any color that he wants so long as it is black".

Resources