In the last part of this series we explored how we could use the Windows Forms .NET framework from Ruby using an intermediate Powershell script. This time we will go down a completely different route. Today we will build GUIs using the popular web technologies - HTML, CSS and JavaScript.
Web technologies over the years have improved greatly. Open source JavaScript engines have become faster at processing data and HTML and CSS have greatly improved to provide clean interfaces for the ever growing market of the web. JavaScript, executed from Chrome's V8 Engine, is currently one of the fastest interpreted languages on the market. This is largely due to the fact that there is a huge demand for bigger, faster webpages.
Recently there has been an emerging trend of web technologies being used for desktop applications. Frameworks such as, Electron and NW.js make this not only possible, but both easy to implement and easy to debug.
So the first thing to mention is, IF YOU CAN USE FRAMEWORKS, DO SO!! I cannot stress this enough. If you have the luxury of being able to use any software you desire at your organisation, do so! The only exception is if you want to make open source software for the community. In theses cases I'd strongly suggest you use other techniques mentioned in this article.
Over the past few years there have been countless occasions where I have wanted to implement web technologies into my Ruby scripts. In one such application I wanted to give the user the ability to select a selection list from a combobox, similar to the first example presented in my last article.
I came across a technique, written in AHK, where users could instantiate and control Internet Explorer COM Objects dynamically. This allowed a developer to inject HTML into an Internet Explorer page and receive information about what the user is doing with the page.
You can find my code here. But the general concept is as follows:
First we create the internet explorer object using Win32 OLE and navigate to a blank page:
# Create a new Internet Explorer COM object (use Late Binding PROGID/CLSID):
require 'win32ole'
ie = WIN32OLE.new('InternetExplorer.Application')
# Navigate to blank page
ie.navigate("about:blank")
sleep(0.1) until !ie.busy
Now we need to create our HTML. The important part here, is we need to make the submit mechanism change the page's URL. We can do this using document.url=<<MY DATA>>
. In this example we execute this via a button's onclick
HTML attribute, however generally we would add this event listener using button.addEventListener("click",function(ev){...})
.
html =<<-Heredoc
<html>
<body>
<h1>Hello World</h1>
<button onclick="document.url = 'hello world'">Click me!</button>
</body>
</html>
Heredoc
Next we need to write this data to the IE object, which we can do as follows:
# Write HTML to page
ie.document.write(s)
#Force no scroll bar
ie.document.body.scroll = "no"
#focus focusable UI elements
ie.document.all.entries.Focus
#Make IE visible
ie.visible = true
Finally, we continually wait and test for a change in the document's URL. When the URL does finally change, we can react to this by setting retVar
to the contents of the URL. Otherwise this process will continue till IE doesn't exist anymore. If IE doesn't exist, the rescue
statement will be executed instead:
# Wait till url not about:blank
begin
until (ie.document.url!="about:blank") do
sleep(0.1)
end
retVar = ie.document.url
rescue
retVar = -1
end
puts retVar
And this works! It is isn't overly flexible though and really it's just a dirty hack! It'd be much better if we had a more direct link to Ruby, instead of relying on the document's URL all the time...
In the example above we use document.write()
to inject data into the loaded HTML document. We can access the very same document object via ie.document
in Ruby. So you might be wondering whether we can install our own JavaScript 'plugins' into the document object and use them directly from within Ruby... And, well, you can! For example, take the following JavaScript:
document.eval = window.eval
This allows us to evaluate JavaScript directly from Ruby! For example, ie.document.eval("alert 'hello world'")
will indeed call JavaScript's alert
function. Neat huh? So what more can we do? We can inject Ruby variables into IE.
Take the following JavaScript code:
document.setVar = function(name,value){
document[name]=value;
}
Here we are installing a new function into the document object, which allows us to create new JavaScript variables, on the fly. Using this function we can inject Ruby objects and execute Ruby methods directly from JavaScript! This is called a Browser Helper Object (BHO).
class BHO
attr_accessor :running
# Quit can be used to halt the runtime
def quit()
@running=false
end
#Required methods:
def call
nil
end
def value(*args)
nil
end
end
bho = BHO.new
ie.document.setVar("BHO",bho)
sleep 0.1 while ie.document.BHO.running
Injecting this object into Internet Explorer allows the JavaScript engine to call document.BHO.quit()
. This will set @running
to false
, causing the Ruby host to continue onwards.
Furthermore, we can extend our object to give JavaScript new functionality! For example, let's give JavaScript the ability to read and write files!
# Extend the File object giving JS access to File API.
class WIN32OLE_File < File
# Allow calling of class methods
def self.call
nil
end
def self.value(*args)
nil
end
# Allow calling of instance methods
def call
nil
end
def value(*args)
nil
end
end
ie.document.setVar("RubyFile",WIN32OLE_File)
To more easily wrap ruby objects in these dispatchable wrappers I suggest you use WIN32OLE::getDispatch(Class) function I created.
Now we have totally wrapped Ruby's File
object giving us full file access rights in JavaScript. For example, let's say we wanted to read a file. In Ruby we could use File.read("my/file/path")
or you could use File.new("my/file/path").read()
. In JavaScript we can use the wrapped class as follows: document.RubyFile.read("my/file/path")
or document.RubyFile.new("my/file/path").read()
.
Ultimately, we can port all functionality in the Ruby environment to JavaScript. This extends even to the ICM libraries themselves:
class WIN32OLE_WSApplication < WSApplication
# Allow calling of class methods
def self.call
nil
end
def self.value(*args)
nil
end
end
ie.document.setVar("WSApplication",WIN32OLE_WSApplication)
Beware!
WSApplication.current_network
and similar methods may not work correctly as there is no way to ensureWSOpenNetwork
objects are wrapped in the proper way. That is unless you use a Proxy object. I'll discuss more about this in a future article.
So, great! We have a fully functional system for creating dynamic, GUIs, based on powerful web technologies. We can control the JavaScript environment from Ruby, and can control Ruby from JavaScript. And last of all, it uses technology which is innately available on all Windows machines, so it's a perfect candidate for GUIs built for ICM.
For most purposes using Internet Explorer (IE) is good enough for building GUIs for usage inside ICM. However there are downsides.
So what are the downsides? Well there is a reason why Microsoft has scrapped IE for the newer Edge browser. Although IE works decently as a browser, it is far from a good browser by modern standards. You can visually see how feature lacking IE is by comparing it's API to other browsers. IE is simply put, painful to work with. Over the years there have been innovative systems which simplify working with IE, for example BabelJS and SASS. These technologies are pre-processors which compile source code into multi-browser friendly JavaScript and CSS. However some problems that arise in IE, simply cannot be overcome by these pre-processors at their current level of abstraction.
So of course there are a few options here. We could continue using IE, and continue using the outdated technologies that come with it. We have the advantage that we are always 100% compatible with the user (assuming ICM will stay on Windows), and for most projects this is likely the direction that we'd like to go in. Or we could scrap IE, and figure out how to communicate and control more modern browsers like for example Google Chrome.
The other question to bare in mind is "Who is your audience?". If you plan on sharing scripts, then IE is great because you can always guarantee that it'll work the same everywhere. However if you're making scripts for a company, where you can ensure everyone will have Chrome (or similar) installed, perhaps that'd be a better option.
In the end, the choice is yours, but in the interest of exploring all avenues of GUIs in Ruby, let's get cracking and learn how we can build fully functional GUIs in more modern browsers like Chrome.
Let's begin with the theory. If it is possible for Ruby to host a website, then we can view that website locally from any web browser (Internet Explorer, Chrome, Firefox, ...). This allows us to fully control what is shown on the website. Initially I started by emulating my own webserver with sockets, but later I discovered Ruby came with its own web server framework, WEBrick. To use WEBrick we simply call $server = WEBrick::HTTPServer.new(<<OPTIONS>>)
. A simple HTTP server which works in ICM 6.5.6 can be found below:
=begin
THIS IS REQUIRED IN ICM 6.5.6 DUE TO AN ERROR IN WEBRICK. IF LOGGER IS OTHERWISE REQUIRED, YOU CAN MAKE YOUR OWN.
:Logger => WEBrick::Log.new(NullStream.new)
=end
class NullStream
def <<(o); self; end
end
#remove old reference of server if existent
$server = nil
#create new server
$server = WEBrick::HTTPServer.new(
:Port=>12357,
:DocumentRoot => Dir.pwd,
:Logger => WEBrick::Log.new(NullStream.new), #fixes some bugs...
:AccessLog => [],
)
trap 'INT' do $server.shutdown end
$server.start
Now currently our server doesn't actually do anything. But we can connect to it regardless! In Chrome, or another browser, type http://localhost:12357 into the address bar. It should send you to a 404 - Not Found
page. Great! So now our server is active, how can we make it display a page? This is the job of WEBrick::HTTPServer#mount
. First we define a HTTP GET
and HTTP EXIT
handler to deal with our requests:
DefaultBody=<<END_BODY
Hello world.
END_BODY
class RequestHandler < WEBrick::HTTPServlet::AbstractServlet
# Define HTTP GET handler, Use it to retrieve file data/default body
def do_GET(request,response)
resource = request.path.to_s[1..-1]
# If no file requested, send default body
if resource == ""
response.body = DefaultBody
else
#If a file is requested, try to get the file, else return 404
begin
response.body = File.read(resource)
rescue
response.status = 404
end
end
end
# Define HTTP EXIT handler, Use it to exit the server.
def do_EXIT(request,response)
$server.shutdown
response.body = ""
end
end
$server.mount '/', RequestHandler
Add the following code before $server.start
in the first example and now we have a working server! It should display the text "Hello world" when you navigate to the website in your browser! Similarly, it should navigate to and display the contents of files if you use http://localhost:12357/myFile.txt. One problem at the moment though... If you quit the webpage, the Ruby server continues to run and you have to force quit ICM. Less than ideal! To fix this I have provided the HTTP EXIT
handler. Ultimately this allows us to tell the server when it needs to shut down. Add the following HTML to our DefaultBody
:
<script>
//Exit before unload
window.addEventListener('beforeunload',function(){
request = new XMLHttpRequest
request.open("EXIT","")
request.send()
window.setTimeout(window.close)
return null
});
</script>
This ensures that a HTTP request is sent before the browser window closes! Furthermore, an HTTP EVAL
handler can be added to the WEBrick server code:
class Sandbox
def get_binding
binding
end
end
$evalBinding = Sandbox.new.get_binding
#...
def do_EVAL(request,response)
begin
result = $evalBinding.eval(request.body)
response.body = {:type=>"DATA", :data=>result}.to_json
rescue Exception => e
response.body = {:type=>"ERROR",:data=>e.to_s}.to_json
end
end
Which can be used to evaluate Ruby directly from JavaScript!
function evaluateRuby(script,callback){
request = new XMLHttpRequest
request.onload = function(){
callback(this.responseText)
}
request.open("EVAL","")
request.send(script)
}
Finally, we can open Chrome, and navigate to the server directly from the Ruby script! The command line argument --app
can be used to hide the address bar, making our GUI look even more professional. Here we also set autoplay-policy
allowing us to include music in our GUI automatically.
COMMAND_LINE_ARGS = "--app=\"http://localhost:12357\" --autoplay-policy=no-user-gesture-required"
#Find location of Google Chrome:
require 'win32/registry'
chromePath = Win32::Registry::HKEY_LOCAL_MACHINE.open('SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe')[""]
#Try to open the site in chrome:
case true
when system("\"#{chromePath}\" #{COMMAND_LINE_ARGS}")
else
puts "Cannot open browser. Will now exit."
exit
end
A full working WEBrick example can be found in the GIST. So this is super cool right? The only issue is how much boiler plate code is required. However whenever you have too much boilerplate code it's always best to wrap it in a class (or function)! For this reason I created the GUI.rb
class. With this wrapper it becomes extraordinarily simple to create a new GUI with HTML:
require_relative "3_GUIWrapper.rb"
Default = <<EOF
<title>Simple</title>
<h1>Hello world</h1>
<button style="width:100px;height:30px;" onclick="Ruby.eval('27+79',function(response){alert('27+79 ==> ' + response.data)})"></button>
EOF
gui = GUI::GUI.new(Default)
gui.show()
So by this point you can do just about anything with Chrome GUIs, the limit really is, for the most part, your own imagination! And this leads me swiftly on to my last examples. In ShowOff.rb
I show to what extent you can style your GUIs. Styling and design is an important part of GUI design, but unfortunately not one I am well versed in, so most of the original code actually came from ShaderToy. However it does go to show how extreme you can really get with HTML visualisations, and perhaps in the future we'll see real hydraulic visualisations using similar technology directly embedded into ICM Ruby scripts! Another cool water related example can be found here and here.
However, I believe that Console.rb
really shows where this technology shines! Within Console.rb I created a full Ruby REPL, runnable from ICM, which looks modern like the Chrome devtools REPL. The console has some issues still:
- Symbols are displayed as strings
- Hashes are displayed as javascript objects
However, this could very easily change. I know I'll certainly be gradually improving this project in the future!
In this article we've investigated how to use modern web technologies to create GUIs for our Ruby applications. Compared to PowerShell, HTML offers greater flexibility as well as being a system that more people are familiar with.
We've discussed embedding Ruby objects directly into IE and how we can use HTTP requests to execute Ruby scripts from within Chrome.
HTML, CSS and JavaScript are all extremely flexible and built specifically for making good looking and user friendly GUIs. It'd be great to see this technique being used by more applications in the future.