Web Enabling SDK for Windows Apps

Thinfinity VirtualUI delivers your Windows applications on any device, anywhere. All with a single line of code.

 

Windows Applications in the Web InstantlyWindows apps to web

As users are migrating from traditional desktop computers to mobile devices, transitioning your Windows application to the Web is the next logical step to increase your exposure. You can make your Windows applications accessible from the Web TODAY.

Furthermore, you may enhance your existing applications with online data, rejuvenate your older software, and deliver your application to anywhere in the world, all with Thinfinity VirtualUI.
 
Continue reading

Thinfinity VirtualUI meets HTML5 Web Components!

We have exciting news! A new release of Thinfinity VirtualUI is out, now with support for HTML5 Web Components!

VirtualUI Web ComponentsSo what Web Components is about and why is so important for your VirtualUI-based application?

I think I don’t need to explain anything about reusable software components. You develop in C#, Vb.Net, Visual Basic, Delphi, etc, so you’ve been using them for years!

Continue reading

Cybele Software released Thinfinity VirtualUI 2.0

Today is THE day! Cybele Software, Inc. formally announced the release of Thinfinity VirtualUI 2.0. Check the new Thinfinity VirtualUI 2.0

As we anticipated a few weeks ago, we’ve been working to bring you a more powerful tool. The new version of VirtualUI now includes great features suggested and expected by many users:

– Enhanced end-user access schema
– File System and Registry Virtualization
– Session recording/playback

Continue reading

All about Thinfinity VirtualUI

Thinfinity VirtualUI Tutorial: What it is? What does it do?

THINFINITY_virtualUI_Guide_smooth

With Thinfinity VirtualUI, any developer can create dual-platform applications (Windows/HTML5) by adding only one line of code to their existing projects built in .Net, Delphi, Visual C++, Virtual Basic or others.

Thinfinity VirtualU allows these applications to be run normally on a Windows environment, or installed in a Thinfinity VirtualUI Server environment and be accessed remotely from any HTML5-compliant Web browser.

Check out these press releases:

 

Would you like to see some online demos?

Find them here.

 

What about jsRO?

Thinfinity VirtualUI relies on Javascript Remote Objects (jsRO), a brand new technology conceived by Cybele Software that enables the creation of remotable server objects, mirrored to the web as native Javascript objects.

jsRO provides full two-way data-binding and remote procedure calls using an easy and straight-forward methodology -making the integration of Windows desktop and web access as easy as pie!

 

Thinfinity VirtualUI is the key

Video 1

You now have the opportunity to take your app to the web in a few minutes.

Thinfinity VirtualUI web-enables Windows Apps with just a single line of code. And it works with popular Windows development environments and programming languages!

When using VirtualUI there’s no need for costly virtualization or remoting environments such as Citrix XenApp® or Microsoft™ RemoteApp, hence slashing IT costs and simplifying maintenance and administration.

Read more about the many Thinfinity VirtualUI benefits.

 

Thinfinity VirtualUI Guide for a Smooth First Time User Experience

Thinfinity VirtualUI is designed to have a straightforward implementation.

Completing the whole Thinfinity VirtualUI installation and setup will only take a couple of minutes before you can enjoy access to your desktop application from a browser in a pure HTML5 interface.

We’ve prepared an article to tell you about some tips and tricks that can help you save time and ensure you have a smooth first time user experience.

 

###

 

Additional documentation:

Product Pagehttp://www.cybelesoft.com/Thinfinity/VirtualUI/
White paper: http://www.cybelesoft.com/docs/thinfinity_virtualui_whitepaper.pdf
Online manualhttp://www.cybelesoft.com/helps/thinfinity/virtualui/

 

Contact:

For more information about Thinfinity VirtualUI, contact Cybele Software, Inc.
3422 Old Capitol Trail, Suite 1125
Wilmington, DE – 19808, USA.
Phone: (302) 892-9625 or (866) 462-9768 (USA & Canada)
Email: info@cybelesoft.com
Website: www.cybelesoft.com

Preview: HTML5 Remote Desktop Integration, revisited

Thinfinity Remote Desktop Integration, revisitedWould you like to integrate a remote desktop within a Web application and access it from any device?

Previously, we let you know about support for an improved authentication method system that will be available in an upcoming Thinfinity VirtualUI release, and surely in Thinfinity Remote Desktop, too, in the close future.

Today, we want to share with you a quick insight about our new powered and extended SDK. This SDK was created to accomplish a better integration between Thinfinity Remote Desktop and external applications.

Continue reading

How to send cross-domain messages using Javascript

 
Thinfinity  - Cross-Domain Messages

A basic rule in Web security holds that there is no direct javascript access between different windows loaded in a browser unless these windows are from the same origin.

This means that the windows must share protocol, host and port or it will not be possible to access a value or object on another page from the JavaScript code, or to add content. So, how can we establish and maintain the communication between different pages loaded in a cross-domain environment?

Fortunately, benefiting from the window.postMessage API, the windows can send non intrusive messages to each other via javascript in a cross-domain environment. These messages can be text, or an object in JSON format (JavaScript Object Notation) —something already available in most of modern web browsers.

In this article we will show how to apply this API to send cross-domain messages using Javascript in a secure way.

 

The window.postMessage API

In order to send a message to another window, one must invoke a postMessage() method, introduced below:

targetWindow.postMessage(message, targetDomain, [extra])

where:

targetWindow Reference to the window that will receive the message.
message JSON object or text that will be sent to the target window.
targetDomain Domain to which the message should be posted.
It can also be written “*” not to limit messages to a particular domain, although this is not recommended —unless absolutely necessary.
extra A sequence of extra —optional— objects that could be transferred with the message. The ownership of these optional objects is no longer on the sending side, but is transferred to the destination one.

But if a tree falls in a forest and nobody’s around to hear it, does it make a sound? In other words, it is useless to send the message if no one is prepared to listen to it. In order to be heard, the event message must be attended. Although most modern web browsers employ the addEventListener() method to add the treatment of the event, old IE versions makes use of its own; so we will cover both alternatives:

if (window.addEventListener){
    addEventListener("message", listenerFunction, false)
} else {
    attachEvent("onmessage", listenerFunction)
}

Where listenerFunction will be used to process the message coming from another window. This function will receive the message in the data attribute of the event received:

function listenerFunction(e) {
    if (typeof e.data == "string") {
        console.log(“The message is a text: ” +  e.data);
    } else {
        console.log(“The message is a JSON object:” + JSON.stringify(e.data));
    }
}

 

Some considerations regarding security

Cross-window messaging security model is two-sided. In the event of knowing the domains of the both parties involved —which is usually the case—, and in order to make the exchange of information between different domains safer, it is recommended to check, both when sending and receiving, the domains that participate in the messaging exchange. The sender ensures that the receiving domain is targetDomain. If the sender tries to send a message to a domain different to targetDomain, an error will occur.

The receiver can check the origin attribute of the received message event object to make sure that this came from a valid origin. Therefore, if the domain of origin does not match a valid domain, it can be ignored.

 

Let’s do it!

The following example implements cross-domain communication between two pages: one loaded in the localhost domain, and the other one in IP 127.0.0.1. To handle the exchange of messages we will create two javascript classes (MessageSender and MessageReceiver).

The MessageSender class has a single published method (sendMessage) and, during its creation, it will receive the window to where the message should be sent (targetWindow, mandatory) and the domain to which the message should be directed (targetDomain, optional), which if not received will be replaced by “*”. Both arguments are sent within a JSON object.

var MessageSender = function(args) {
    args = args || {};
    var targetWindow = args.targetWindow;
    var targetDomain = args.targetDomain || "*";
    var ready = false;
    var sendMessage = function(message) {
        targetWindow.postMessage(message, targetDomain);
    }
    return {
        "sendMessage": sendMessage
    }
}

The MessageReceiver class has two published methods. The start method initiates the listening of the message, while the stop method ends it. Both methods could receive a callback function to do an additional processing in the start/stop messaging. This class, when instantiated, is capable of receiving a JSON object with a pair of attributes, both optional. The first is a callback function to process the message; the other, a validOrigin string that indicates the domain from which it would be valid to receive messages. Should any of these parameters is not sent, the first one will be replaced by a default message processor , while the other, by the same page’s domain (i.e., it will not be cross-domain and will only receive messages from its own domain). In case you do not want to control the origin of the message, the validOrigin attribute must explicitly assert “*”.

var MessageReceiver = function (args) {
    args = args || {};
    var started = false;
    var validOrigin = args.validOrigin || window.location.origin;
    var defaultMsgProcessor = function (e) {
        if (typeof e.data == "string") {
            alert("The message is test:\n'" + e.data + "'");
        } else {
            alert("The message is a JSON object:\n" + JSON.stringify(e.data));
        }
    }

    var msgProcessor = args.callback || defaultMsgProcessor;
    var processMessage = function (e) {
        if (validOrigin == "*" || validOrigin == e.origin) {
            msgProcessor(e);
        }
    };
    var startListening = function (onStartCallback) {
        if (!started) {
            if (window.addEventListener) {
                window.addEventListener("message", processMessage, false);
            } else {
                attachEvent("onmessage", processMessage);
            }
            started = true;
            if (onStartCallback) onStartCallback();
        }
    }
    var stopListening = function (onStopCallback) {
        if (started) {
            if (window.removeEventListener) {
                window.removeEventListener("message", processMessage);
            } else {
                window.detachEvent("onmessage", processMessage);
            }
            started = false;
            if (onStopCallback) onStopCallback();
        }
    }
    return {
        "start": startListening,
        "stop": stopListening
    };
};

Both classes will be included in the same javascript file, which we will call crossDomainMessenger.js.

In order to use these classes, we will create two html pages. The first one, crossDomainSource.html, includes the crossDomainMessenger.js file and displays, on the top part, a line containing a text field (where the messages to be sent will be added) and some buttons to send messages. At the bottom of this page, the second page will load in an iframe, which would receive the messages.

From javascript, it will create both a MessageSender and a MessageReceiver; the first one to send messages triggered by the buttons to the page loaded in the iframe; and the last one to address messages that could be sent to the internal page.

This second page will also create a MessageSender and MessageReceiver, with the difference that in this case, the important work that will done by the latter.

The page also has buttons to toggle the message listener; a button to clear the list of messages received; and, a last button to send a message to the other page, which would close the communication circuit between the two windows.

Below you will find the code for the two pages of the example:

crossDomainSource.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Cross domain message example - Sender</title>
    <style>
        html, body { height: 100%; margin: 0px; padding: 0px; }
        #divmsg { height: 50px; line-height: 50px; vertical-align: middle; padding-left: 10px; padding-right: 10px; }
        #divtarget, #targetWindow {display: block; position: absolute; }
        #divtarget { top: 51px; bottom: 5px; left: 5px; right: 5px; border: solid 1px; box-sizing: border-box; }
        #targetWindow { width: 100%; height: 100%; border: none; }
    </style>
    <script type="text/javascript" src="crossDomainMessenger.js"></script>
    <script type="text/javascript">
        var msgSender = null;
        var msg = null;

        function init() {
            var targetDomain = window.location.protocol + "//127.0.0.1:" + window.location.port;
            var tw = document.getElementById("targetWindow");
            tw.src = targetDomain + "/crossDomainTarget.html";
            msgSender = new MessageSender({ "targetWindow": window.frames["target"], "targetDomain": targetDomain });
            msg = document.getElementById("txtmsg");

            // Creates a MessageReceiver that accepts messages from targetDomain only
            new MessageReceiver({ "validOrigin": targetDomain }).start();
        }

        function sendText() {
            msgSender.sendMessage(msg.value);
        }

        function sendObject() {
            msgSender.sendMessage({ "message": msg.value, "currentDate": new Date().toJSON() });
        }

        function sendClear() {
            msgSender.sendMessage({ "cmd": "CLEAR" });
        }
    </script>
</head>
<body onload="init()">
    <div id="divmsg">
        <label for="txtmsg">Message:</label>
        <input type="text" id="txtmsg" size="30" />
        <input type="button" value="send text" onclick="sendText()"/>
        <input type="button" value="send object" onclick="sendObject()"/>
        <input type="button" value="send clear" onclick="sendClear()"/>
    </div>
    <div id="divtarget">
        <iframe id="targetWindow" name="target"></iframe>
    </div>
</body>
</html>

crossDomainTarget.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Cross domain message example - Receiver</title>
    <style>
        html, body { height: 100%; margin: 0px; padding: 0px; }
        #msgPanel > div { border-bottom: dashed 1px #808080; }
    </style>
    <script type="text/javascript" src="crossDomainMessenger.js"></script>
    <script type="text/javascript">
        var msgSender = null;
        var msgReceiver = null;
        var msgPanel = null;
        function init() {
            msgPanel = document.getElementById("msgPanel");
            // This MessageReceiver accepts messages from localhost only.
            msgReceiver = new MessageReceiver({
                "validOrigin": window.location.protocol + "//localhost:" + window.location.port,
                "callback": function (e) {
                    if (typeof e.data.cmd != "undefined" && e.data.cmd == "CLEAR") {
                        clearMsgs();
                    } else {
                        addMessage("The message is " +
                            ((typeof e.data == "string") ?
                                "test:\n'" + e.data + "'" :
                                "a JSON object:\n" + JSON.stringify(e.data)));
                    }
                }
            });
            start();
            msgSender = new MessageSender({ "targetWindow": window.parent });
        }

        function start() {
            msgReceiver.start(function () { addMessage("*** msgReceiver was started!") });
        }

        function stop() {
            msgReceiver.stop(function () { addMessage("*** msgReceiver was stopped!"); });
        }

        function clearMsgs() {
            msgPanel.innerHTML = "";
        }

        function sendHellow() {
            msgSender.sendMessage(window.location.origin + " says Hello!");
        }

        function addMessage(message) {
            var newmsg = document.createElement("div");
            newmsg.innerHTML = message;
            msgPanel.appendChild(newmsg);
        }
    </script>
</head>
<body onload="init()">
    <div>
        <input type="button" value="start" onclick="start()"/>
        <input type="button" value="stop" onclick="stop()"/>
        <input type="button" value="clear" onclick="clearMsgs()"/>
        <input type="button" value="send msg to opener window" onclick="sendHellow()" />
    </div>
    <div id="msgPanel"></div>
</body>
</html>

This is enough to understand how the messaging between windows works, even when they are in different domains. In upcoming posts we will demonstrate how to use this technique to establish a smooth communication between Thinfinity VirtualUI or Thinfinity Remote Desktop with other applications to which they are integrated.

 

Browser within the browser: breaking the Matryoshka effect.

When the browsers are nested.
image01

It has become increasingly common to expect desktop applications to be capable of integrating to the web to incorporate available external information. There are several ways this could be achieved: by consuming web services to get the raw data; or, by simply including a browser component within the application, in the form of a panel. In this second case, we’ll be capable of navigating to sites displaying up-to-date information or external multimedia content, such as maps, videos, images and sound —as an example, visit Fish Facts, a live demo that shows basic GUI virtualization capabilities as well as an integration with the web browser and external web resources.

Once we transform these applications with Thinfinity® VirtualUI™, and we invoke them from the web, an effect similar to the famous Russian Nested Dolls case occurs: we run from the browser an application that contains within itself another browser. In short, we surf to an application that internally re-surfs. This is not bad in itself, but we can take advantage of already being in the web to access the final resource directly from the main browser. By doing this we will avoid a roundabout that, among other things, would unnecessarily increase the consumption of resources.

Let’s take a look at the diagrams of the same application running from the desktop…

Matryoshka-image1

… and from the Web:

Matryoshka-image2

As we can see, when we run the application from the desktop we must necessarily navigate through an embedded Webbrowser component. The application is the one that connects to the desired web address, and this webpage is seen in an internal panel, within the application.

But once we load the application from a browser we would experience the aforementioned effect. As a result, the application browses on its own, and the visible result of that surfing is re-transferred to our browser.

If we transfer that navigation to the only browser we need to use, we will access the intended resource without delay or additional bandwidth, processing, or time consumption.

image00

The demo application

The application we use in the example (whose complete source can be found here) attempts to break this browser-within-the-browser effect, simply by using the internal browser only when the application is accessed from the desktop. If the application is being accessed from the web, instead of using that component, the app would load the desired page in an iframe created on the fly. To achieve this integration (which requires a little bit more interaction between the application and the browser) we will add a jsRO object, which will control the remote iframe from the application.

The first addition to our application will be the adding of VirtualUI and its initialization:

using System;
using System.Windows.Forms;

namespace matryoshka
{
   static class Program
   {
       /// <summary>
       /// The main entry point for the application.
       /// </summary>
       [STAThread]
       static void Main()
       {
           new Cybele.Thinfinity.VirtualUI().Start();
           Application.EnableVisualStyles();
           Application.SetCompatibleTextRenderingDefault(false);
           Application.Run(new Form1());
       }
   }
}

Let’s now see what we have to add to the main form code, which it only has a panel where to enter the browsing URL and another one where the WebBrowser component is located.

The following is the application code before adding the changes:

using System;
using System.Drawing;
using System.Windows.Forms;

namespace matryoshka
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            // Sets Google Maps URL to run in the embedded browser
            txtURL.Text = "https://maps.google.com";
            Navigate();
        }

        private void btnGo_Click(object sender, EventArgs e)
        {
            Navigate();
        }

        private void txtURL_KeyPress(object sender, KeyPressEventArgs e)
        {
            if (Convert.ToInt32(e.KeyChar) == 13)
            {
                Navigate();
            }
        }

        private void Navigate()
        {
            if (!txtURL.Text.Equals(""))
            {
                if (!txtURL.Text.StartsWith("http"))
                {
                    txtURL.Text = "http://" + txtURL.Text;
                }
                webBrowser1.Navigate(new Uri(txtURL.Text));
            }
            else {
                webBrowser1.Navigate(new Uri("about:_blank"));
            }
        }
    }
}

As a first step, we will add to the class the objects in the Cybele.Thinfinity library:

using System;
using System.Drawing;
using System.Windows.Forms;
using Cybele.Thinfinity;

namespace matryoshka
{
    public partial class Form1 : Form
    {

        private VirtualUI vui = new VirtualUI();
        private IJSObject mRemoteLayout = null;

        public Form1()
        ...

This demo application loads a Google Map. To be run embedded in an iframe, Google Maps needs a different URL from the one used on a full page. We know that it will run embedded in the iframe (using VirtualUI) and directly when loaded into the internal browser.

public Form1()
{
    InitializeComponent();
    if (vui.Active)
    {
        // Code executed only when the application runs with Thinfinity VirtualUI
        // Sets Google Maps URL to run embedded in an iframe
        txtURL.Text = "https://www.google.com/maps/embed";
    }
    else
    {
        //* Code executed only when the application runs from desktop 
        // Sets Google Maps URL to run in the embedded browser
        txtURL.Text = "https://maps.google.com";
    }
}

Additionally, we must also make some changes to Navigate( ):

private void Navigate()
{
    string sURL = "";
    if (txtURL.Text.Equals("")){
        sURL = "about:_blank";
    } 
    else
    {
        sURL = txtURL.Text;
        if (!sURL.StartsWith("http"))
        {
            sURL = "http://" + sURL;
        }
    }
    if (vui.Active) {
        mRemoteLayout.Properties["url"].AsString = sURL;
    }
    else {
        webBrowser1.Navigate(new Uri(sURL));
    }
} 

The last (but not least important) addition is the one concerning the jsRO object management: the first method, for its instantiation and definition of their getters; the other one, for the later updating of the iframe’s bounds.

private void Form1_Shown(object sender, EventArgs e)
{
    // Creates jsRO object for iframe control
    mRemoteLayout = new JSObject("layout");
    mRemoteLayout.Properties.Add("windowId").AsString =
                String.Format("virtualui_canvas_{0}", Handle);
    mRemoteLayout.Properties.Add("url").AsString = txtURL.Text;
    mRemoteLayout.Properties.Add("bounds")
        .OnGet(new JSBinding(        // Adds a "getter" to bounds
            delegate(IJSObject AObj, IJSProperty AProp)
            {
                // Returns a JSON object
                Point p = pnlNav.PointToScreen(new Point(0, 0));
                p.Offset(-Left, -Top);
                AProp.AsJSON = "{" + String.Format("\"left\":{0}, \"top\":{1}, \"width\":{2}, \"height\":{3}",
                     p.X, p.Y, pnlNav.Width, pnlNav.Height) + "}";
            }));
    mRemoteLayout.ApplyModel();
}

private void pnlNav_Resize(object sender, EventArgs e)
{
    // Updates all mRemoteLayout's properties "getters"
    if (mRemoteLayout != null)
    {
        mRemoteLayout.ApplyChanges();
    }
}

We can now introduce the additional information that must be included, at javascript level, to the HTML page.

What follows is the base page to which we will be adding the functionality:

 

We will now create three global variables for the VirtualUI and jsRO objects, and for the iframe created on the fly.

<script type="text/javascript">
var virtualUI = null;
var jsro = null;
var nav = null;

$(document).ready(function () {
    ...

We will add three functions intended to create the iframe and keep it updated:

...
$(document).ready(function () {

    var navigate = function(url) {
        nav.src = url;
    }

    var applyBounds = function(bounds) {
        nav.style.left = bounds.left + "px";
        nav.style.top = bounds.top + "px";
        nav.style.width = bounds.width + "px";
        nav.style.height = bounds.height + "px";
    }

    var createNav = function () {
        nav = document.createElement("iframe");
        nav.id = "nav";
        nav.style.position = "absolute";
        nav.style.display = "none";
        nav.style.zIndex = 2;
        nav.style.border = "none";
    }
    ...

We will now instantiate the VirtualUI object, configure its events, and then connect to the application:

var createNav = function () {
   ...
   ...
}

virtualUI = new Thinfinity.VirtualUI();

virtualUI.onError = function (errorMsg) {
    alert("Application load failed");
};

virtualUI.onLoading = function () {
    console.log((virtualUI.devMode) ? "Waiting for application..." : "Loading...");
};

virtualUI.onClose = function (url) {
    if ((typeof url != 'undefined') && (url != '') && (url != null)) {
        if ((virtualUI.devMode != true) || (window.top.opener)) {
           window.top.close();
        }
        window.top.location.href = url;
        return;
    }
    if (virtualUI.devMode) { location.reload(); }
    if ((virtualUI.devMode != true) || (window.opener)) { window.close(); }
    if ((window.top == window) && (document.referrer) && (location.href != document.referrer)) {
        location.href = document.referrer;
    } 
    else {
        if (nav) { nav.style.display = "none"; }
        alert("Application closed");
    }
};

// -- Connect to server...
virtualUI.connect();

Finally, we will create the jsRO object instance. Please note that it’s in the creation of the layout jsRO model (defined in the application) where the iframe is injected into the application window. The other two events are used to keep both the URL and the inserted iframe bounds updated:

...
virtualUI.connect();

// Defining Javascript Remote Objects Elements
jsro = new Thinfinity.JsRO();

jsro.on('model:layout', 'created', function (obj) {
    layout = jsro.model.layout;
    if (nav == null) {
        createNav();
        document.getElementById(layout.windowId).appendChild(nav);
        nav.style.display = "inline-block";
    }
    applyBounds(layout.bounds);
});

jsro.on('model:layout.url', 'changed', function (obj) {
    navigate(obj.value);
});

jsro.on('model:layout.bounds', 'changed', function (obj) {
    applyBounds(layout.bounds);
});

With this procedure, we’ve been able to break the “Matryoshka Effect”. From now on, the embedded browser will be used from the desktop, while the external one will be used from the web —but always being controlled by the remote application.

 

Running the application from the browser

To see the end result, we must first add the application to the list of registered apps in the Thinfinity VirtualUI Server and configure its home page, making sure it points to the page modified by us in this tutorial.

Once this is done, we can access the application from the browser. If we are running the application from the Thinfinity VirtualUI Development Lab, we must change the virtual path’ address so as to match the one defined in the configuration —in the VirtualUI’s manager.

 

Thinfinity® VirtualUI Video: Getting Started with Thinfinity VirtualUI (.Net)

We’d like to invite you to check out our latest video “Getting Started with Thinfinity VirtualUI (.Net)” that can be found in our Thinfinity Google+ page.

 

How to add the VirtualUI library to a .Net application

In this new installment of our video tutorials we show how to add the Thinfinity® VirtualUI™ library to a .Net application and to publish it to be accessed from the Web.

Don’t forget! Check this blog and visit our Google+ page often to find tips, news and videos that will help you get the most out of Thinfinity VirtualUI!

More online content is yet to come!

Please feel free to send us comments or feedback and connect with us on your social network.

Thinfinity® VirtualUI Video: Getting Started with Thinfinity VirtualUI (Delphi)

We’d like to invite you to check out our new video “Getting Started with Thinfinity VirtualUI (Delphi)” that can be found in our Thinfinity Google+ page.

 

How to add the VirtualUI library to a Delphi application

If you are interested in finding tips and videos that will help you get the most out of Thinfinity Remote Desktop and Thinfinity VirtualUI, check this blog and our Google+ page often! We will continue expanding our online content and keep you updated with the latest information.

Please feel free to send us comments or feedback and connect with us on your social network.