Tech Note 34:
Cool Things you can do with NS Basic/CE

June 26, 2007

© NSB Corporation. All rights reserved.

 

The NS Basic/CE environment has been designed so there's a lot of power in some very simple instructions. Once you've got the hang of it, there are some advanced things you can do with NS Basic/CE that would be tough to do in most other languages.

Here are some of the cool things we've discovered. Let us know if you come up with other things!

 

1. Use EXECUTE to use parameters as references

Private Sub setupButton(name, text)
  execute name & ".borderstyle=1"
  execute name & ".drawText" & chr(34) & text & chr(34)
end sub

This subroutine sets the name of a button to text and draws a line around the box. It is called with the name of the button and the text you want to show. CHR(34) is the quote sign (").

EXECUTE takes a string and feeds it to the interpreter, just as if it were a statement in your program.

If you do

SetupButton("myButton","Tap here")

This is what gets executed within the subroutine:

myButton.drawText "Tap Here"

The two execute statements could actually be combined into a single statement, by concatenating a return (vbCrLF) between the two strings.

 

2. Create Objects on the fly

NS Basic/CE lets you create objects at any time, not just when you build the project. Building on our previous example, here's a subroutine that creates the button object as well as sets it up:

Private Sub setupButton(name, text, x,y,w,h)
  addObject "PictureBox",name, x,y,w,h
  execute name & ".borderstyle=1"
  execute name & ".drawText" & chr(34) & text & chr(34)
end sub

Since we can create objects only as needed, memory requirements can be kept lower. It may also may it easier to design the UI.

 

3. Create subroutines on the fly

This one is really wild, and builds on the previous two Cool Things. The following code creates n buttons on the screen, all of them clickable:


Option Explicit
dim count,i
count=inputbox("How many do you want?")
for i=1 to count
  makeButton "B" & i,cstr(i),i
next
 
Private Sub makeButton(name, prompt,b)
  dim code
  addObject "PictureBox",name & "Btn",(b mod 25)*25, int(i/25)*25,20,20

  execute name & "Btn.borderstyle=1"
  execute name & "Btn.drawText" & chr(34) & prompt & chr(34)
  code="sub " & name & "Btn_click()" & vbcrlf & "print " & chr(34) & "click at " & name & chr(34) & vbcrlf & "end sub"
  execute code
end sub

 

In this case, we're doing everything we did in the first two. However, we're taking advantage of one additional thing: The runtime environment is composed of both the variables and the code. In Cool Thing 1, we modified the values of variables in the runtime from our program. In this one, we're actually modifying the code, by adding a subroutine for each button that we create. When the button is then tapped, the event is sent to the new subroutine we created.

 

4. Use Control Arrays

It is not obvious that you can create control arrays in NS Basic. This program creates 6 commandbuttons and executes a set command to put them into an array called cmdButtons. (Contributed by Terry Myhrer)

option explicit

dim cmdButtons(5), index

updatescreen

for index = 0 to 5
  MakeButton(index)
  cmdButtons(index).caption = "Button " & cstr(index)
next

sub ButtonClick(byval number)
  if cmdButtons(number).caption = "Clicked" then
    cmdButtons(number).caption = "Button " & cstr(number)
  else
    cmdButtons(number).caption = "Clicked" 
  end if
end sub

sub MakeButton(byval number)
  dim name, code
  name = "btnButton" & cstr(number)
  addobject "CommandButton", name, 10, number * 30, 100, 25
  execute " Set cmdButtons(" & cstr(number) & ") = " & name
  code = "Sub " & name & "_Click()" & vbcrlf & "  ButtonClick " & cstr(number) & vbcrlf & "end sub"
  execute code
end sub   

5. How to make multiline CommandButtons using WindowLong

It's possible to make a button with several words allow CR breaking and have multiple lines. (Dave Joyce)
Const BS_MULTILINE = &H00002000
in Form_Load, do the following (This can be done in another sub, but then you have to move the object at least 1 pixel to get it to redraw and use the new setting)
CommandButton1.WindowLong(0) = BS_MULTILINE
WindowLong is a powerful feature to manipulate the appearance of objects. You'll need to look at Microsoft's documentation to get the full information.

6. How to control tabbing explicitly

Objects that are children do not always tab as expected. Also, sometimes you may want to use a tab order that is different than usual. Here's a technique from Dave Joyce that puts you in control.
Dim nextCtrl

sub activeCtrl_GotFocus
    nextCtrl = "otherControl"
end sub

sub output_keyup (keycode, shift)
    if keycode = 9 then '9 is tab key
       keycode = 0 'should prevent system from firing anything
       execute nextctrl & ".SetFocus"
    end if
end sub
If your controls are out of order, the execute line fires but then the keycode 9 goes to the system and refires the setFocus to the systems next in the creation order control list and then end result is that focus doesn't go to the nextCtrl. Keep reading - the next item is also important...

7. Setting ComboBox.ListIndex on manual entry

The user keyboard entry into a combobox control could be one on the list or may not be, but the simple fact of entry does not select the combobox.ListIndex to match. Enter this little routine. Note you can use a for loop to go through all the List values but the result is the same, ListIndex gets reset. (Dave Joyce)
Declare "Function SendMessageStr Lib ""Coredll"" Alias ""SendMessageW"" 
(ByVal HWND As Long, ByVal wMsg As Long, ByVal wParam As Long, ByVal 
lParam As String) As Long"

Dim bReset

Const CB_FINDSTRING = &H14C
Const CB_FINDSTRINGEXACT = &H158
Const CB_SETEDITSEL = &H0142

sub combobox_change
    if bReset then exit sub 'prevents endless loops
    bReset = true
    getIdx "combobox"
    bReset = false
end sub

sub getIdx(ctrl)
   Dim idx
    'finds the idx of the exact match (case insensitive)  inside the combobox.List
   Execute "idx = sendMessageStr(" & ctrl & ".hwnd, CB_FINDSTRINGEXACT, 0 , " & ctrl & ".Text)"
   If idx > -1 Then 'only assign when match found otherwise constantly resetting to -1
      Execute ctrl & ".Listindex = " & idx 'setting the ListIndex causes the box to select all text
     'following turns off text selection inside control
      Execute "SendMessageStr " & ctrl & ".hwnd, CB_SETEDITSEL, 0, -1"
   End If
end sub
This getIdx routine is called when the user types into a combobox text window which fires the onChange event. The problem is, setting the ListIndex inside of the onChange event only lasts inside the event, once the event dies, the ListIndex becomes what is was before the getIdx routine.

SOLUTION

I fought this and the tabbing with all sorts of possible tags, techniques and procedures until I suddenly realized I had to do both of these procedure after the subroutine in which they are called. My solution is to delay execution of my routine until after the system has finished with its routine by using the NSBasic timer feature.

To the 1st Example add a timer event, this technique does not require re ordering creation of controls.

Dim nextCtrl

sub activeCtrl_GotFocus
    nextCtrl = "otherControl"
end sub

sub activeCtrl_Timer
    activeCtrl.Timer = 0  'turn off timer event so it fires only once
    if nextCtrl = "" then exit sub 'prevents invalid code
    execute nextCtrl & ".SetFocus" 'because using generic nextCtrl this routine can be used by any other tabbing event
end sub

sub otherCtrl_GotFocus
    nextCtrl = "activeCtrl"  'same keyup event will allow tabbing back to activeCtrl regardless of creation order
end sub

sub output_keyup (keycode, shift)
    if keycode = 9 then '9 is tab key
       keycode = 0 'should prevent system from firing anything
       execute activeCtrl.Timer = 25 'now your setFocus happens just after the system setFocus
    end if
end sub
To the 2nd Example again add a timer event and add another global variable.
Dim bReset, thisCtrl 'by adding this variable you can use the routine in 
'the one timer event from other comboboxes without duplicating timer events

Const CB_FINDSTRING = &H14C
Const CB_FINDSTRINGEXACT = &H158
Const CB_SETEDITSEL = &H0142

sub combobox_change
    if bReset then exit sub 'prevents endless loops
    thisCtrl = "combobox"
    combobox.Timer = 25 'timer event fires in 25 ms, plenty of time for system to complete ending the change event
end sub

sub combobox_Timer
    combobox.Timer = 0 'turn off timer so it only fires once
    bReset = true 'the code below will cause a onChange event this flag allows our code to exit gracefully
    getIdx thisCtrl 'generic call using thisCtrl allows code to be reused by other comboboxes
    bReset = false 'reenables the rest of the onChange event based on user input
end sub

sub getIdx(ctrl)
   Dim idx
   Execute "idx = sendMessageStr(" & ctrl & ".hwnd, CB_FINDSTRINGEXACT, 0 , " & ctrl & ".Text)"
   If idx > -1 Then
      Execute ctrl & ".Listindex = " & idx
      Execute "SendMessageStr " & ctrl & ".hwnd, CB_SETEDITSEL, 0, -1"
   End If
end sub