Thursday, May 10, 2012

Adding a CommandLink Button on Windows

This brief tutorial will show you how to add native controls on Windows using only Realbasic code.  This assumes some basic knowledge of Win32 APIs.  You can download the complete project here.

In this example we'll be adding a native CommandLink button.  Here are some examples of what a CommandLink button looks like:

For information on when you would use such a button please refer to Microsoft's page on this:

To create our native control we'll start by subclassing Canvas, it will act as our "host."  We will create and setup the control in the Canvas Open event (not the Constructor because the Canvas window handle, i.e. Canvas.Handle, isn't ready at that point):

Sub Open()
  Declare Function CreateWindowExW Lib "User32" ( ex as Integer, className as WString, _
  title as WString, style as Integer, x as Integer, y as Integer, width as Integer, _
  height as Integer, parent as Integer, menu as Integer, hInstance as Ptr, _
  lParam as Integer ) as Ptr

  Declare Function GetModuleHandleA Lib "Kernel32" ( name As Ptr ) as Ptr

  Dim hInstance As Ptr = GetModuleHandleA( Nil )

  Const WS_CHILD = &h40000000
  Const WS_VISIBLE = &h10000000

  mCommandHandle = CreateWindowExW( 0, "BUTTON", "", _
  0, 0, 0, 0, Me.Handle, 0, hInstance, 0 )
End Sub

Notice that we passed Me.Handle to CreateWindowExW as the parent, this allows the Canvas to "host" the control and handle things like clipping, visibility, and Mouse/Drag events.  However, setting the Focus on the CommandLink button is not automatic. Since the Realbasic framework only knows about the Canvas control, tabbing into it does not automatically give the CommandLink button the focus. You will have to add code in the GotFocus event for that:

Sub GotFocus()
  Declare Sub SetFocus Lib "user32" (hwnd As Ptr)
  SetFocus( mCommandHandle )
End Sub

You will also have to manage resizing the control yourself:

  // This code lives in the Open event
  Declare Sub SetWindowPos Lib "User32" ( hwnd as Ptr, after as Integer, _
  x as Integer, y as Integer, width as Integer, height as Integer, flags as Integer )

  Const SWP_NOZORDER = &h4
  SetWindowPos( mCommandHandle, 0, 0, 0, Me.Width, Me.Height, SWP_NOZORDER )

One thing to keep in mind is that the Canvas doesn't have a resized event, so you will have to detect for yourself when the control is resized, for example in the Paint event.  (Side note: for those following Feedback Case #16099 you can use a ContainerControl subclass instead of our Canvas subclass for this).

This next section deals with setting up events for our CommandLink button, we just want to know when the button is clicked. Realbasic tries to shield you from a lot of the nitty gritty details, like calling functions that were built with different calling conventions (i.e. how parameters are pushed and popped off the stack when the function returns).  However, when you deal with Callbacks (i.e. when the OS calls into Realbasic land), you will have to play nice since it expects a certain calling convention for that function. In the Windows world the calling convention is StdCall. In Realbasic we can setup a Shared Method and supply the correct calling convention for that function:

Function WndProc(hWnd as Integer, msg as Integer, wParam as Integer, lParam as Integer) As Integer
  #pragma X86CallingConvention StdCall

  Dim obj As CommandLink = sWndProcMap.Value( hWnd )
  If obj <> Nil Then
    Return obj.InternalWndProc( msg, wParam, lParam )
  End If
End Function

Since this is a shared method it doesn't know what object called it, but we can store that information in a Dictionary:

  // This code lives in the Open event
  If sWndProcMap = Nil Then sWndProcMap = New Dictionary
  sWndProcMap.Value( Me.Handle ) = Self

  Declare Function SetWindowLongW Lib "user32" ( hwnd As Integer, nIndex As Int32, dwNewLong As Ptr ) As Ptr

  Const GWL_WNDPROC = -4
  mOldWndProc = SetWindowLongW( Me.Handle, GWL_WNDPROC, AddressOf WndProc )

Note that we're hooking up the events for the Canvas control (i.e. we're passing Me.Handle instead of mCommandHandle), this will not always be the case but for CommandLink Buttons the WM_COMMAND message that we want gets passed to the parent:

Function InternalWndProc(msg as Integer, wParam as Integer, lParam as Integer) As Integer
  Const WM_COMMAND = &h111
  Const BN_CLICKED = 0

  Select Case msg
    If HIWORD(wParam) = BN_CLICKED Then
      RaiseEvent Action
    End If
  End Select

  Declare Function CallWindowProcW Lib "User32" ( oldProc As Ptr, handle As Integer, msg As Integer, wParam As Integer, lParam As Integer ) As Integer
  Return CallWindowProcW( mOldWndProc, Me.Handle, msg, wParam, lParam )
End Function

This is what our finished product looks like:

Download the complete project to see how to add the detail note and swap the arrows with a custom picture, enjoy!

1 comment:

KSA++ said...

Thank you for this Tutorial

How to Make Image + text is right-aligned