Wednesday, 29 May 2013

HTA - HTML-defined GUIs for your scripts

Back when I was assessing whether I needed Python in my life, I was trying to find a straightforward way to build a GUI. All the tools listed at seemed to be total overkill for basic needs or not licensed in a way that I found acceptable for the commercial use I had in mind.

Some time later, I was passing the time on StackOverflow seeing if I could rack up some more points for being helpful with some problem solving, and up came a question about GUIs for batch files. HTA was a solution - quite a good one in fact. But it's funny that it seems that MS haven't been too hot about spreading the word about HTA - I have to say that even with all the geek sites that I surf daily it's not something I recall ever seeing getting much of a mention.

An HTA is simply HTML, displayed by the guts of Window's IE engine via mshta.exe - allowing you to display anything a browser can normally show, collect input, and run code that can't normally be run within HTML scripts. For example, write to local files. This means an HTA can be a script with a GUI, or it can be used to create a GUI for a separate script - command batch or Python, etc ...

Good ol' Wikipedia will explain more about HTA (HTML Applications), and MS gives you the basics - so all I can do here is to present a full little example showing how simple it would be to add some spice to a script (Command .bat Batch, Python or other) that needs some user input. Here we can see how simple it is to display the HTML GUI from the script, passing in any required parameters if required, and retrieve the user's input afterwards.

Example HTA

The HTML below is all you need to produce a basic example GUI like this :

  <title>My HTA Test</title>


    Sub NoContextMenu()
      window.event.CancelBubble= True
      window.event.ReturnValue= False
      ' Alert("Right-click is not allowed, sorry!")
    End Sub

    Sub Window_onLoad
        ' use the following line to disable right-click menu and prevent View Source
        Set document.onContextMenu=GetRef("NoContextMenu") 

        ' resize to quarter of screen area, centered
        window.resizeTo screen.availWidth/2,screen.availHeight/2
        window.moveTo screen.availWidth/4,screen.availHeight/4
        'Msgbox oHTA.commandLine

        ' Passed-in code - will execute to set variables
        arrParams = Split(oHTA.commandLine, "`")
        For i = 1 to (Ubound(arrParams))
          ' Msgbox arrParams(i)

        ' SCRIPT-specific code : set controls from executed code
    End Sub 

    Sub Set_Env_Vars
      ' msgbox "writing " & txt_str1.value
      Set oWSS = CreateObject("WScript.Shell") 
          ' FYI: msgbox oWSS.ExpandEnvironmentStrings("%WinDir%")
      strBat="Echo @SET ""ZZ_NUM1=" & txt_num1.value _
          & """ ^& @SET ""ZZ_STR1=" & txt_str1.value & """ > ""%TEMP%""\testhta-set.bat"
      strBat=strBat & " & Echo @SET ZZ_NUM1=^& @SET ZZ_STR1=^&> ""%TEMP%""\testhta-clr.bat" "cmd /C " & strBat ,0,true
      Set oWSS = Nothing 
    End Sub 

<BODY bgcolor="#ccccFF">
  Just a little test....
  Batch Path &nbsp; <input type="text" value="" name="txt_bp"">
  Number &nbsp; <input type="text" value="" name="txt_num1"">
  String &nbsp; <input type="text" value="" name="txt_str1"">
  <input type="button" value="Return these to the batch"
   name="run_button"  onClick="Set_Env_Vars"> 

The <Head> section contains the HTA:APPLICATION tag to specify a number of options about the HTA.

In the <Script> section we have :

Sub NoContextMenu()
(along with the first line of Window_onLoad)
Prevents the user from seeing the source code by disabling any right-click pop-up menu which would have had a View Source option. The user won't have any clues to reveal that it's an HTA.

* Sub Window_onLoad
Sets the window size and location, runs the code passed in from the batch, and then uses those newly set variables to set the control values.

* Sub Set_Env_Vars
This is called as the click action for the button, and uses a WScript.Shell object to run a single line of Command Line code (two or more commands can be executed in one line with a & between them) in order to create a temporary batch file in the %TEMP% folder. This - containing environment variable assignments - will be called from the main batch file after the HTA has closed, providing a mechanism to get the values available to the batch file.
Sure, you could write the file the hard way in the VBScript, but why not call a simple piece of familiar command line code instead - with all the %TEMP% decoding for free?

In the <Body> section, we have normal HTML with <Input> controls, and whatever ever else you would need to make things look beautiful.

If you'd rather use JavaScript, yes, that's just as capable. Here's the part that resizes and centers the HTA, and sets the function to run automatically when loading/refreshing :

<SCRIPT LANGUAGE="javascript">
    function Window_onLoad(){  // resize to quarter of screen area, centered

Example Batch File

You could, of course, call the HTA directly and rely upon it doing whatever it needs to do itself, or arrange for it to call hidden batch files to do any heavy lifting. But this blog post is about calling an HTA from an already-running batch file for user interaction.

This testhta.bat calls the above HTA, passing in some code that gets executed within the HTA - setting some variables. Everything after the backtick ` is interpreted as code by the HTA. Several lines of code can be compressed into one line by using a : between each command.

In this example the %~dp0 trick is passed in (shown in the GUI but not passed back to the batch) to show how you could pass in the batch's path in case you decide to try building in functions within the batch file (chunks of code that get bypassed in normal use but can be used by calling the batch with suitable parameter to enable a goto), to be called from within the HTA - another can of worms of complexity! (with the advantage of containing all your code in just two files, the .bat and .hta)

When control returns to the batch, it calls the temporary batch file that the HTA created, and in this way the cmd environment variables are set and will then be available to use within the batch. Here's an example of the "%TEMP%\testhta-set.bat" contents:
@SET "ZZ_NUM1=5.678" & @SET "ZZ_STR1=Oh, I see." 

@echo off
start /wait mshta.exe "C:\somepath\test.hta" ` batch_path="%~dp0" : num1=3.14 : str1="String Param" : msgbox("Wow!")
if exist "%TEMP%\testhta-set.bat" call "%TEMP%\testhta-set.bat"
echo Result: %ZZ_STR1%

 rem Tidy up and remove Env Vars & temp batches
 if exist "%TEMP%\testhta-clr.bat" call "%TEMP%\testhta-clr.bat"
 if exist "%TEMP%\testhta-clr.bat"  del "%TEMP%\testhta-clr.bat"
 if exist "%TEMP%\testhta-set.bat"  del "%TEMP%\testhta-set.bat"

Yes, I realise that including runable code - in the call to the HTA file - is something that would raise an eyebrow or two amongst purists, but this is just one simple way to pass in data to be used by the HTA - there are other ways. This is for personal use, or where security issues aren't a problem. Just showing the easiest way to get you started!

Python version

Some subtle differences here in :

#Python v3.3
print("Hello, World!")

# First, call the HTA - using | in place of any "  - and wait for it to close
from subprocess import Popen    # now we can reference Popen
from subprocess import PIPE
new_process_obj = Popen(['mshta.exe', r'C:\your-path\testpy.hta', r'` batch_path=|no %~dp0 available| : num1=3.14 : str1=|String Param| : msgbox(|Wow!|)'])

# retrieve a line of code to exec, from py file in temp folder
process = Popen(['cmd.exe','/c type %TEMP%\\ & echo '], shell=False, stdin=PIPE,stdout=PIPE,stderr=None)  
output = process.stdout.readline()  
estr= output.decode("utf-8")[:-3]  # delete some trailing "\r\n garbage from the & echo

# now we have the variables set

# pause in case run from Win Explorer double-click
input('Press <ENTER> to continue')

When the HTA is launched, the VBScript code is passed in with the | character used in place of ", for Pythonesque reasons. Also, when the string is retrieved within the HTA it will have an unwanted trailing " - which calls for two replaces :

        For i = 1 to (Ubound(arrParams))
            ' next 2 lines needed for Python method of calling the HTA
          arrParams(i)=Replace(arrParams(i),Chr(34),"") ' delete any "
          arrParams(i)=Replace(arrParams(i),"|",Chr(34)) ' any | becomes "

One final difference is the method used to pass the values back to the Python script. This time I've used a file in %TEMP% to be exec-uted. Here's an example, one line with two commands separated by a semi-colon  :
ZZ_NUM1=3.14; ZZ_STR1="String Param"

This is created by this modified part of the orginal HTA (in Sub Set_Env_Vars)  :

      strBat="Echo ZZ_NUM1=" & txt_num1.value _
          & "; ZZ_STR1=" & chr(34) & txt_str1.value & chr(34) & " > ""%TEMP%""\"

Of course, as both the HTA and Python have access to the Clipboard, that could be used for passing data back or forth instead, as one of many alternatives.


I don't want to duplicate someone else's fine work, so have a good read here :
In this case the author is starting from the HTA and launching the PowerShell from there, instead of the other way around. Whatever seems logical to you!

I have rewritten the HTA in JavaScript and I provide a PS script in my follow-up posting

No comments:

Post a Comment