Search

Sunday 24 February 2013

Sirius: Win32 - add Dialog handling and basic input

Recently I've added functionality working with main windows. Next step is to apply functionality to interact with dialog boxes and child controls. It's almost the same except these are child windows and we should carry out additional relationship to main window. Additionally, we should add basic ability to send mouse and keyboard input. Thus, we'll complete basic functionality for Win32. All other code to be written should cover some control specifics rather than common functionality.

Scope of changes

The following areas require changes:

  1. Server side
    1. Updates to existing window search functionality for child windows
    2. New window search functionality for dialogs
    3. Base functionality for keyboard and mouse input
  2. Win32 client library
    1. New window search functionality for dialogs
    2. Updates to existing window search functionality for child windows
    3. Base functionality for keyboard and mouse input
    4. New classes for controls
So, let's take a look at them one by one.

Server side

Updates to existing window search functionality for child windows

Before we had the support for the functionality searching for top level windows. But it's not applicable for child windows as they're looked under some specific window and simple EnumWindows function doesn't work here. However, we already reserved the placeholder for parent field in the Win32Locator class. That would be the indicator showing if we should look for top level window or not. The only additional operation we should add is the EnumChildWindows call with specified parent window. So, all we have to do here is to update Win32Utils class and in particular that should be searchWindow method. The updates are:

 public long searchWindow(Win32Locator locator){
  User32Ext user32 = User32Ext.INSTANCE;

  locator.setHwnd(0L);
  WNDENUMPROC enumProc = new WNDENUMPROC(locator);
  Pointer pt = Pointer.NULL;
        
  if( locator.getParent() == 0L ){
   user32.EnumWindows(enumProc, pt);
  }
  else {
   HWND hWnd = new HWND();
   hWnd.setPointer(Pointer.createConstant(locator.getParent()));
   user32.EnumChildWindows(hWnd, enumProc, pt);
  }
  return enumProc.getLocator().getHwnd();
 }
So, now if we specify the non-zero parent window handle in the locator we'll perform search under specific window.

New window search functionality for dialogs

For modal dialogs the situation is a bit different. Major difference is that model dialog is the top level window as well. However logically it's tightly bound to some specific application window. So, if we look for some specific message box we're likely to find many instances of potential candidates. It's especially true for standard message box dialogs which can have multiple instances in the system. How to narrow down the search to locate exactly window we need? For this purpose we'll use the knowledge that any modal dialog runs in the same thread as main caller window (that's what actually makes dialog model). Given that we should look for specific window in the same thread as main window. For this purpose we can use EnumThreadWindows function. It works the same way as EnumWindows except it narrows down search scope to the specific thread. At the end, we'll update Win32Utils class with new search method like:

 public long searchSameThreadWindow(long baseHwnd,Win32Locator locator){
  User32Ext user32 = User32Ext.INSTANCE;
  
  HWND hWnd = new HWND();
  hWnd.setPointer(Pointer.createConstant(baseHwnd));
  
  IntByReference lpdwProcessId = new IntByReference();
  int threadID = user32.GetWindowThreadProcessId(hWnd, lpdwProcessId);
  
  Pointer pt = Pointer.NULL;
  locator.setHwnd(0L);
  
  WNDENUMPROC enumProc = new WNDENUMPROC(locator);
  user32.EnumThreadWindows(threadID, enumProc, pt);
  
  return enumProc.getLocator().getHwnd();
 }
Now we should be able find dialog boxes.

Base functionality for keyboard and mouse input

Final point for current server side changes is the functionality for keyboard and mouse input. Actually, it's all about SendMessage functionality. Since it's the functionality common for any window I'll put updates to the Window class.

Firstly, I'll add functionality for mouse down and mouse up. Generally we should define which button should be pressed/released as well as different combinations of Ctrl/Alt/Shift keys which are difined as classes. Generally the code looks like:

 public void mouseDown(long hwnd,int button,int x,int y, boolean isControl,boolean isAlt, boolean isShift){
  int message = 0;
  int flags = 0;
  switch(button){
   case 0:
    message = WM_LBUTTONDOWN;
    break;
   case 1:
    message = WM_RBUTTONDOWN;
    break;
   case 2:
    message = WM_MBUTTONDOWN;
    break;
   default:
    message = WM_LBUTTONDOWN;
    break;
  }
  
  if(isControl){
   flags |= MK_CONTROL;
  }
  if(isShift){
   flags |= MK_SHIFT;
  }
  SendMessage(hwnd, message,flags, MAKELPARAM(x, y).intValue());
 }
 
 public void mouseUp(long hwnd,int button,int x,int y, boolean isControl,boolean isAlt, boolean isShift){
  int message = 0;
  int flags = 0;
  switch(button){
   case 0:
    message = WM_LBUTTONUP;
    break;
   case 1:
    message = WM_RBUTTONUP;
    break;
   case 2:
    message = WM_MBUTTONUP;
    break;
   default:
    message = WM_LBUTTONUP;
    break;
  }
  
  if(isControl){
   flags |= MK_CONTROL;
  }
  if(isShift){
   flags |= MK_SHIFT;
  }
  SendMessage(hwnd, message,flags, MAKELPARAM(x, y).intValue());
 }
Then I'll add functionality for click. It's actuallly the combination of mouse down and mouse up functionality. The code is:
 public void click(long hwnd,int button,int x,int y, boolean isControl,boolean isAlt, boolean isShift){
  mouseDown(hwnd, button, x, y, isControl, isAlt, isShift);
  mouseUp(hwnd, button, x, y, isControl, isAlt, isShift);
 }
And I'll add functionality for double click (it looks the same as mouse up/down except the message to send):
 public void doubleClick(long hwnd,int button,int x,int y, boolean isControl,boolean isAlt, boolean isShift){
  int message = 0;
  int flags = 0;
  switch(button){
   case 0:
    message = WM_LBUTTONDBLCLK;
    break;
   case 1:
    message = WM_RBUTTONDBLCLK;
    break;
   case 2:
    message = WM_MBUTTONDBLCLK;
    break;
   default:
    message = WM_LBUTTONDBLCLK;
    break;
  }
  
  if(isControl){
   flags |= MK_CONTROL;
  }
  if(isShift){
   flags |= MK_SHIFT;
  }
  SendMessage(hwnd, message,flags, MAKELPARAM(x, y).intValue());
 }
Now we have functionality for mouse input.

Then we should add functionality for keyboard input. For now I'll limit functionality with simple methods for pressing/releasing/typing specific character. It's all about WM_KEYDOWN,WM_KEYUP,WM_CHAR messages. So,the code looks like:

 public void keyDown(long hwnd,int key){
  SendMessage(hwnd,WM_KEYDOWN,key,0);
 }
 
 public void keyUp(long hwnd,int key){
  SendMessage(hwnd,WM_KEYUP,key,0);
 }
 
 public void keyPress(long hwnd,int key){
  SendMessage(hwnd,WM_CHAR,key,0);
 }
So, that's it for server side. Now we should apply the above changes to the client side.

Win32 client library

Before doing next steps we should generate the client for server functionality.

Updates to existing window search functionality for child windows

All changes are related to the Window class for client library. In particular, we're interested in the exists method. Given the new changes for parent window handling the flow of this method is:

  1. If parent window is defined go to point 2. Otherwise, go to point 3
  2. If parent window doesn't exist return false
  3. If current window exists return true. Otherwise, return false
So, the implementation looks like:
 public boolean exists() throws RemoteException {

  if (parent != null) {
   if (!parent.exists()) {
    return false;
   } else {
    this.locator.setParent(parent.getHwnd());
   }
  }

  this.locator.setHwnd(0);
  if(parent != null){
   this.client = parent.client;
  }

  long hwnd = 0;
  try {
   hwnd = client.utils().searchWindow(locator);
  }
  catch(Throwable e){
   logger.debug(String.format("Error while searching for window", locator),e);
  }
  if (hwnd != 0) {
   this.locator.setHwnd(hwnd);
   return true;
  } else {
   this.locator.setHwnd(0);
  }
  return false;
 }
The changes would be reflected for the window objects which have non-NULL parent field. We actually have to check parent window for 2 reasons:
  1. We simply check that parent window exists at all. Otherwise, there's nothing to look for child window at
  2. Another role of exists method is to initialize HWND data for any window with up to date values. That should be done for parent windows as well.

New window search functionality for dialogs

Since we have new window class of DialogBox we should define it in the client code. The windows of such class can be either top level windows (for dialog-based applications) or can be inline controls on the form (like for page control tabs). In other words such windows can be either with or without parent window defined. That should be reflected in constructors. So, the base code is:

package org.sirius.client.win32.classes;

import java.rmi.RemoteException;

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

public class DialogBox extends TopLevelWindow {

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

 public DialogBox(Win32Client client, Window parent, Win32Locator locator) {
  super(client, parent,locator);
 }
}
For now, all that's left to add is the exists method which uses recently added window search functionality. So, overridden exists method looks like:
 @Override
 public boolean exists() throws RemoteException {
  if(this.parent == null){
   return super.exists();
  }
  else if(!parent.exists()){
   return false;
  }
  else {
   long hwnd = client.utils().searchSameThreadWindow(parent.getHwnd(), locator);
   if(hwnd==0){
    return false;
   }
   else {
    locator.setHwnd(hwnd);
   }
  }
  logger.debug("Dialog was found: " + locator);
  return true;
 }
I've highlighted 2 key parts:
  1. If the dialog is top level main window (no parent defined) it should be searched as any other top level window
  2. If the dialog is child window (or modal) it should be looked within the same thread as parent window
The entire code for DialogBox class can be found here.

Base functionality for keyboard and mouse input

To apply mouse/keyboard input functionality we should update the Window class. For now we'll just make simple left button click and ordinary text input functionality. For this purpose we'll update the Window class with the following code:

  • for left button click:
     public void click() throws Exception {
      if(!exists()) return;
      client.core().window().click(locator.getHwnd(), 0, 0, 0, false, false, false);
     }
    
  • for key typing:
     public void typeKeys(String text) throws Exception {
      if(!exists()) return;
      for(char key:text.toCharArray()){
       int code = key;
       // TODO Add specific keys handling
       client.core().window().keyPress(locator.getHwnd(), code);
      }
     }
    

New classes for controls

Now we're fully ready to start implementing functionality for specific controls. But before that we just have to prepare the placeholders for them. Firstly, we'll create the Control class with the following content:

package org.sirius.client.win32.classes;

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

public class Control extends Window {

 public Control(Window parent, Win32Locator locator) {
  super(parent, locator);
 }
}
For now it's empty. Only constructor is defined. But it will be the base class for all other control classes. Their skeleton can be found under the org.sirius.client.win32.classes.controls package. At the moment we just need the classes themselves. As long as development goes, those classes will be filled with methods.

Usage sample

OK. Once we're done with the code changes it's time for some demonstration sample. We'll perform the following scenario:

  1. Start Notepad application
  2. Navigate menu File > Open and make sure Open dialog appears
  3. Click on Cancel button and make sure the Open dialog disappears
  4. Enter Hello World!!! text into the text field
  5. Close the window and make sure the dialog prompting saving changes appears
  6. Click on No and make sure that Notepad window is closed

Window declarations

In previous posts I've already had a deal with windows declarations but we needed to describe only one window. Current example contains at least 3 windows. So, firstly, these are 2 dialogs:

  • Open Dialog
  • Save Message box
Both of them contain buttons we need to click on during the test. So, let's define them. The definitions look like:
 public class OpenDialog extends DialogBox {

  public Button btnCancel;
  
  public OpenDialog(Win32Client client, Window parent,
    Win32Locator locator) {
   super(client, parent, locator);
   btnCancel = new Button(this,new Win32Locator("Button",1));
  }
 }
 
 public class SaveMessageDialog extends DialogBox {

  public Button btnNO;
  
  public SaveMessageDialog(Win32Client client, Window parent,
    Win32Locator locator) {
   super(client, parent, locator);
   btnNO = new Button(this,new Win32Locator("Button",1));
  }

 }
Highlighted items show the controls initialization.

Next step is to define the class for Notepad. This time it contains edit field as well as above described dialogs are the fields as well. So, the code looks like:

 public class NotepadWindow extends MainWindow {
  
  public OpenDialog openDialog;
  SaveMessageDialog dSave;
  public Edit edtText;
  
  public NotepadWindow(Win32Client client, Win32Locator locator) {
   super(client, locator);
   
   openDialog = new OpenDialog(client, this, new Win32Locator("#32770(.*)",0));
   dSave = new SaveMessageDialog(client, this, new Win32Locator("#32770(.*)",0));
   edtText = new Edit(this,new Win32Locator("Edit",0));
  }
 }
Before starting implementing test we should initialize the main window instance as well as bind it to the client. We should add instructions like:
 Win32Client client = new Win32Client();
 NotepadWindow notepad = new NotepadWindow(client,new Win32Locator("Notepad",0));
That's it! Now we can write the test.

Test implementation

Here is the test implementation step by step:

  1. Start Notepad application
      notepad.start("notepad.exe", "", "");
      System.out.println("Waiting for appear");
      Assert.assertTrue(notepad.exists(60000));
    
  2. Navigate menu File > Open and make sure Open dialog appears
      notepad.menu().getSubMenu(0).pick(1);
      Assert.assertTrue(notepad.openDialog.exists(60000));
      Assert.assertTrue(notepad.openDialog.isVisible(60000));
      // Here we check Cancel button presence
      Assert.assertTrue(notepad.openDialog.btnCancel.exists());
      Assert.assertTrue(notepad.openDialog.btnCancel.isVisible());
      Assert.assertTrue(notepad.openDialog.btnCancel.isEnabled());
    
  3. Click on Cancel button and make sure the Open dialog disappears
      notepad.openDialog.btnCancel.click();
      Assert.assertTrue(notepad.openDialog.disappears(60000));
    
  4. Enter Hello World!!! text into the text field
      notepad.edtText.typeKeys("Hello World!!!");
    
  5. Close the window and make sure the dialog prompting saving changes appears
      notepad.close();
      Assert.assertTrue(notepad.dSave.exists(60000));
      Assert.assertTrue(notepad.dSave.btnNO.exists(60000));
    
  6. Click on No and make sure that Notepad window is closed
      notepad.dSave.btnNO.click();
      Assert.assertTrue(notepad.disappears(60000));
    

Summary

That's it. This time we've got the following results:

What was plannedDone/FailedComments/What should be done
Apply searching for dialogsDone 
Apply search for child windowsDone 
Add ability to define specific controls and interact with themDone 
Further growth is related to the functionality expansion to different control types. That would complete the Win32 functionality library.

No comments:

Post a Comment