Search

Wednesday 30 January 2013

Sirius (Java): writing Win32 client library + Live Demo

Recently I described the creation of Win32 server part and at the end I wrote the function which finds the main window by specified attributes and gets the handle for it. This is quite fundamental thing as once we have a handle of the window we can send any messages to it simulating various GUI interaction actions. At the same time the server side library contains only set of functions as Win32 API is all about the set of functions. But it's too raw for writing tests. It's much more convenient to apply some object model to the window objects. So, it requires some effort on the client side. In this article I'll describe the process of Win32 client API creation. I'll create classes interacting with top level windows and create some sample test to demonstrate how that functionality works.

The final output will contain the functionality interacting with top level Win32 windows as well as some demo showing how to work with it.

The entire development will require 2 major stages:

  1. Server side update to provide more convenient interface for Win32 functionality
  2. Client library development

Server side updates

In one of the previous posts I described how we can extend JNA library with additional API functions. But during development I encountered another restriction. Since I'm passwing the JNA Win32-specific structures through SOAP I'm getting only some interface part of the types I passed not the internal structure. So, if you pass the HWND structure via SOAP the client will generate the type which can access to all the public variables. But if you try to get the actual value (there's an integer value which is contained within the HWND) you'll fail as there's no such interface accessible. So, the major server side update is about replacing Win32-specific types and values with primitive types or transportable structures.

Another reason for such enhancement is that a lot of Win API functionality operate with the pointers to the structures while for our API there can be more convenient forms.

I'll describe server side improvements based on the GetWindowRect example. With raw JNA functionality calls it can be operated in the following form:

  HWND handle = // Here we're getting window handle somehow
  RECT result = new RECT();
  user32.GetWindowRect(handle, result);
  // after that we can use the result variable to get the rectangle information
But it's much more convenient to use something like this:
  long handle = // This time the handle is of the Long type which is transportable through SOAP
  RECT result = GetRect(handle);
  // after that we can use the result variable to get the rectangle information
For this purpose I'll do the following:
  1. Open Sirius-Server-Win32 project and add new package named org.sirius.server.win32.classes.
  2. In this package I'll add the Window class which will contain all necessary Win32 method wrappers related to any window. In the future I'll add some other classes there encapsulating logic working with different control types.
  3. For our purpose the class would have the following content:
    package org.sirius.server.win32.classes;
    
    import com.sun.jna.Pointer;
    import com.sun.jna.platform.win32.WinUser;
    import com.sun.jna.platform.win32.WinDef.HWND;
    
    public class Window implements WinUser {
    
     protected User32Ext user32 = User32Ext.INSTANCE;
    
     public Window() {
      // TODO Auto-generated constructor stub
     }
    
     public RECT getRect(long hwnd){
      RECT result = new RECT();
      HWND handle = ...
      user32.GetWindowRect(handle, result);
      return result;
     }
    }
    
    Note that the hwnd parameter is passed as long while user32.GetWindowRect method accepts the HWND type as the first parameter. The part of the code highlighted with red should be updated with the code which transforms JNA HWND type to long. That would be the next step
  4. Convert long parameter to HWND value. For this purpose we should use the construction like:
      long input = ... // Here should be some value
      HWND handle = new HWND();
      handle.setPointer(Pointer.createConstant(input));
    
    The HWND contains the pointer to the address in memory where the window is stored. And this is the number we need to operate.
  5. Apply changes. After that our Window class looks like:
    package org.sirius.server.win32.classes;
    
    import com.sun.jna.Pointer;
    import com.sun.jna.platform.win32.WinUser;
    import com.sun.jna.platform.win32.WinDef.HWND;
    
    public class Window implements WinUser {
    
     protected User32Ext user32 = User32Ext.INSTANCE;
    
     public Window() {
      // TODO Auto-generated constructor stub
     }
    
     public RECT getRect(long hwnd){
      RECT result = new RECT();
      HWND handle = new HWND();
      handle.setPointer(Pointer.createConstant(hwnd));
      user32.GetWindowRect(handle, result);
      return result;
     }
    }
    
  6. Next step is to extend all other window related functionality in the same fashion. Probably the conversion from long to HWND should be placed into separate method (that's what I did).
  7. After all changes are done we should add @WebService annotation to the class and add the class to the list of Sirius component endpoints in the configuration file
Thus we'll have additional endpoint providing interface to windows. The entire source as well as similar classes can be found in the repository.

Client side preparations

Once we have the new service endpoint we should generate the client for it. Thus we'll get WindowProxy class in the SiriusClient-Win32 location. In order to fit the client design described here we should add this class into the Win32Core client class. Thus we'll be able to access the methods with the following code:

Win32Client client = new Win32Client();
client.core.window().// here should be the call of WindowProxy class method
Once it's done we're ready for client API implementation.

Creating Win32 client API

General classes hierarchy overview

If we want to make classes as some abstractions of the windows we should identify the hierarchy. There're some methods common for any window, there're methods specific for some classes only. So, we should design the API based on that relationship. General hierarchy looks like:

In this post I'll describe the following classes:
  • Window
  • MovableWindow
  • TopLevelWindow
  • MainWindow
Before that I'll create org.sirius.client.win32.classes package in the SiriusJavaClient-Win32 project where all those classes will be added to.

Window class

The Window class will be the most common window class which should contain functionality common for the most of windows. Also it should identify the most common attributes as well as it defines the way the any window class should be initialized.

Core attributes and constructors

Alright, let's start with it. Firstly, we'll define the class itself and it's core attributes:

public class Window implements WinUser {

 protected Win32Client client;
 protected Win32Locator locator;
 protected Window parent;

}
We always should have a reference to Win32Client object as it's the most common interface to communicate with server side. We need the locator as the major attribute defining how we should find the window. And, at the moment we just reserve the reference to parent window. We'll not use it in this example but in the future it will be needed.

The above attributes are needed to be initialized. We need the constructor. Actually, we'll make constructors which do the following:

  • initialize window with client and locator
  • initialize window with client, locator and parent window
Here is their code:
 public Window(Win32Client client, Win32Locator locator) {
  this(client,null,locator);
 }

 public Window(Win32Client client, Window parent, Win32Locator locator) {
  this.client = client;
  this.locator = locator;
  this.parent = parent;
 }
There's actually common constructor and there's one specific constructor which sets parent window as null by default.

Additionally, I'll add some auxiliary method which just simplifies the code constructions. The HWND of the window is the item which should be widely used accross the methods. So, I'll just add one method which provides fast access to it:

 public long getHwnd(){
  return this.locator.getHwnd();
 }

Window presence verification

At this point we had only one method interacting with the server. It is Win32UtilsProxy::searchWindow method which finds the window by the specified search attributes and the main thing it does is that it returns the window handle. That's the fundamental part for the client side as we have to find the window and then just use the handle for reference. At the same time this reference is valid as long as the window exists. Once window disappears the handle is no longer valid. So, the most appropriate place where we should initialize/reset handle is the methods checking window presence.

So, I'll add the exists methods which returns true if window was found or false otherwise. The code looks like:

 public boolean exists() throws RemoteException {
  // Reset existing HWND
  this.locator.setHwnd(0);
  long hwnd = 0;
  
  // Searching for window
  hwnd = client.utils().searchWindow(locator);
  if (hwnd != 0) {
   // If window exists we store the handle and return true
   this.locator.setHwnd(hwnd);
   return true;
  }
  else {
   // Otherwise the window handle is kept equal to 0
   this.locator.setHwnd(0); 
  }
  // If we're here we haven't found anything. Returning false
  return false;
 }
The exact place where search is initiates is highlighted with yellow.

Usually, we not only check that some window exists. Quite frequently we're interested in making sure that window was closed. So, let's make appropriate method for that:

 public boolean disappears() throws Exception {
  return !exists();
 }
This method is fully reversal to the exists method. If window disappears it returns false and the window handle is reset to 0. Just what we need.

Window state information

Then I'll just add some methods returning the information about current window state. E.g. we'll add methods returning window/client rectangle area and checking if window is enabled or visible. These are simple wrappers on core Win32 client functionality and the code looks like:

 public Rect getClientRect() throws Exception {
  Rect rc = client.core().window().getClientRect(this.locator.getHwnd());
  return rc;
 }

 public Rect getRect() throws Exception {
  Rect rc = client.core().window().getRect(this.locator.getHwnd());
  return rc;
 }

 public boolean isEnabled() throws Exception {
  return client.core().window().isEnabled(locator.getHwnd());
 }

 public boolean isVisible() throws Exception {
  return client.core().window().isVisible(locator.getHwnd());
 }

Waiting for status functionality

And some sugar. Usually when we run automated tests and check for some specific state of the window the state appears not at once. It takes a while before window appears or closes. Similar thing for many other states. So, normally we should wait for some time before required state is achieved. As the result, we need some method which waits till specific method with specific parameters return expected result. This method looks like:

 private Class<?>[] getArrayTypes(Object... params) {
  Class<?>[] types = new Class[params.length];
  for (int i = 0; i < params.length; i++) {
   types[i] = params[i].getClass();
  }
  return types;
 }

 public boolean waitFor(long timeout, String methodName,
   Object expectedValue, Object... params) throws Exception {
  long end = (new Date()).getTime() + timeout;
  Class<?>[] parameterTypes = getArrayTypes(params);
  while ((new Date()).getTime() < end) {
   Method waitMethod = this.getClass().getMethod(methodName, parameterTypes);
   Object result = waitMethod.invoke(this, params); 
   if (result.equals(expectedValue)) {
    return true;
   }
  }
  return false;
 }
Looks complex (it involves reflection to call methods on fly) but for current purposes the usage is simple. We'll add 2 more methods which wait for window to appear and disappear respectively. And this time there would be the timeout value specified as the parameter. So, these methods look like:
 public boolean exists(long timeout) throws Exception {
  return waitFor(timeout, "exists", true);
 }

 public boolean disappears(long timeout) throws Exception {
  return waitFor(timeout, "disappears", true);
 }
That's it for this class at the moment. Full implementation details can be viewable here.

MovableWindow class

This class is for all windows which can be moved or resized. It covers most main application windows as well as top level dialogs which can be moved apart from the main window. It extends the Window class and according to the definition it should contain the following methods:

  • moveTo - moves window to the specified position
  • sizeTo - resizes window to the specified width and height
  • maximize/minimize/restore
  • close - simply closes the window
All the above methods are just wrappers on the core functionality calls. Also, there's no specific constructor needed for it. So the entire class looks like:
package org.sirius.client.win32.classes;

import org.sirius.client.win32.Win32Client;
import org.sirius.client.win32.types.Win32Locator;

public class MovableWindow extends Window {

 public MovableWindow(Win32Client client, Win32Locator locator) {
  super(client, locator);
 }

 public MovableWindow(Win32Client client, Window parent, Win32Locator locator) {
  super(client, parent, locator);
 }

 public boolean moveTo(int x, int y) throws Exception {
  this.client
    .core()
    .window().moveTo(locator.getHwnd(), x, y);
  return true;
 }

 public boolean sizeTo(int width,int height) throws Exception {
  this.client
    .core()
    .window().sizeTo(locator.getHwnd(), width, height);
  return true;
 }

 public void minimize() throws Exception {
  this.client
  .core()
  .window().minimize(locator.getHwnd());
 }

 public void maximize() throws Exception {
  this.client
  .core()
  .window().maximize(locator.getHwnd());
 }

 public void restore() throws Exception {
  this.client
  .core()
  .window().restore(locator.getHwnd());
 }

 public void close() throws Exception {
  this.client
  .core()
  .window().close(locator.getHwnd());
 }
}
The full code is available here.

TopLevelWindow class

The TopLevelWindow class usually corresponds to the windows which are not only movable but can be activated separately. E.g. it can be any main window or modal dialog. So, for now all we should know about windows of that class is that they can be activated. So, the class is:

import org.sirius.client.win32.Win32Client;
import org.sirius.client.win32.types.Win32Locator;

public class TopLevelWindow extends MovableWindow {

 public TopLevelWindow(Win32Client client, Win32Locator locator) {
  super(client, locator);
 }

 public void setActive() throws Exception {
  this.client.core().window().activate(locator.getHwnd());
 }
}
The up-to-date code is available here.

MainWindow class

As it was mentioned before, the top level window can be either application main windows or modal/child dialogs. The key thing which makes main application window different from other windows is that it can be invoked by running some executable. So, all we have to add is the method which invokes the application. Additional thing that simplifies a bit is that the main window doesn't contain parent window. So, we need only one constructor. The class code looks like:

package org.sirius.client.win32.classes;

import org.sirius.client.win32.Win32Client;
import org.sirius.client.win32.types.Win32Locator;

public class MainWindow extends TopLevelWindow {

 public MainWindow(Win32Client client, Win32Locator locator) {
  super(client, locator);
 }

 public void start(String executable, String params, String workingDir)
   throws Exception {
  this.client
    .core()
    .window().start(this.locator.getHwnd(), executable, params,
      workingDir);
 }
}
The up-to-date code is available here.

Creating sample test (Live Demo)

Once we're done with the classes for the main windows we can make simple test which invokes the functionality we wrote. The video below demonstrates the window functionality based on Notepad application. It shows very basic sample of test and it doesn't cover such topics like:

  • Window locator definition
  • Complex window objects definition (including child windows and controls)
That would be the topic for separate materials. This time I'll just show how to declare window objects at all and how to use them based on JUnit test example. So, here is the demo video:

Afterword

That's the good start. At this point we already can invoke the application and do some basic things with it. So, this is the indicator that the Sirius really does something visible.

No comments:

Post a Comment