ToHex

Converting numbers, especially ones of a specific size, to hex has long been problematic. The built in functions don’t do a very good job with it and often drop leading zeros even when the integers size is known.


Dim s1 As String 
s1 = Hex(13) // here 13 is an "Integer" literal 
             // and should be treated as a 32 bit integer in a 32 bit build
             // or a 64 bit integer in a 64 bit build

Dim i8 As Int8 = 13
Dim s2 As String
s2 = i8.ToHex

Break

If you look at the values in S1 and S2 produced by the framework functions you’ll see that all leading 0’s are dropped event though the integers are of known size and a proper Hex representation of these should include the leading 0’s. But they don’t.

Most time dropping the leading 0’s is not a big deal. If you are just saving the value into a db, file etc and then eventually converting it back into a value using VAL it wont matter. But when you need it for some other purose then it really does matter. Like when you’re generating a hex key value or something where those leading 0 digits ARE relevant.

So I created a module that does a “proper” hexing of numeric values that does retain the leading 0’s.

Enjoy it

Game of Code

Or “write code like you immediately forget what you wrote” so it’s not a guessing game to figure out why you wrote what you wrote way back when.

What do I mean by that ?

Write code CLEARLY. Use constants liberally and give them good meaningful names.

So instead of writing a line like

If asc(key) = 16 or asc(key) = 204 then

where 16 and 204 are not obvious to everyone maybe write this as

Const ctrlP = 16
Const F5 = 204

if asc(key) = ctrlP  or asc(key) = F5 then

And then even when you go back and look at this code in 6 months even you won’t have to guess what 16 and 204 mean.

Maintaining backwards compatibility in 2019r2

Subclasses, modules and compatibility flags 😛

In Xojo it never used to require a lot of hard work to work across multiple versions. If you simply avoided using the new features then a project could mostly move back and forth between versions without much effort. You could even deal with a lot of newer version changes with a few #If XojoVersion wrappers around bits of code.

2019R2 makes this a little more challenging.

Most of the issues I’ve experienced are because one clients code has a lot of custom control subclasses and these subclasses defined new events. And now, with the takeover of so many event names, these subclass events conflict with the names Xojo took over. It makes it so these client projects no longer just open and compile in 2019r2. They get thousands of compilation errors.

This makes updating to 2019r2 tricky for this client. Especially since they also want to maintain the ability to compile and work in older versions of Xojo to be able to compile for older versions of the OSes they support.

If you’re in the same position there is a way to make it so the event names that were taken over won’t affect client projects.

How ? I created a set of subclass of the built in controls and classes which all client subclasses could be based on and, depending on the build version of Xojo, I could switch these custom subclasses on and off and the rest of the clients subclasses are unaware this has occurred.

What I am not sure of is how far back this technique will work.
Really old versions MAY not interpret compatibility flags quite right.

Now for the HOW TO guide :

  1. In this project I opened it in 2019r2
  2. I created a module called For2019r2
  3. I cleared ALL the compatibility flags for the module so its not compatible with ANY targets
  4. In this module I created new subclasses of every control used and set their scope To “global”
  5. In each subclass I add new event definitions for all the OLD events
  6. In these subclasses I implemented the new event handlers that got renamed to raise the old events added in step 5.

I implemented the “new events” and simply had them raise an event with the old name & params. I could have name these events anything I wanted. By doing this I could add my own methods, properties, etc or even rename any properties Xojo also renamed. I could also handle any exceptions raised and just set error codes or whatever I wanted. I’ve written before about how to do this shadowing “properly”. Thats the technique in use here. What we’re adding this time is the ability to have two different sets of these properly subclassed and shadowed controls that we can switch between.

I saved this project and then in 2019r1.1 I reopened this project as there is some work you cannot do in 2019r2. In the subclasses in the For2019r2 module we also need to add the OLD events which still exist but, because you started the project in2019r2, you can’t see them nor can you add them in 2019r2. We need to also implement the old event handlers and have them raise the event with the old name as well. This is important otherwise in R2 you will get errors about the event definition existing and colliding.

For older IDE’s we create a new module named For2019r1AndEarlier and do much the same adding subclasses of all the controls we need. This time we add just the old event name and a matching event definition for the old event.

The important thing to do is in the module fore 2019r2 if you named the Canvas subclass MySubClass then in the 2019r1 module name the canvas subclass the exact same thing. The same is true for all other control subclasses. They need to be named the same in the For2019r2 and For2019r1AndEarlier modules. This way when you create a new instance on a layout and make its super MySubclass, depending on which module has its compatibility flag enabled you will actually be creating an instance derived from a different superclass.

Now when I want to compile in R2 I select the For2019r2 module & enable the Desktop 32 and 64 bit compatibility flags so my custom subclasses which are outside this module use the base classes inside this module.
When I want to compile using 2019r1.1 and earlier I disable the compat flags on the For2019r2 module and enable the compt flag on the For2019r1AndEarlier so my other classes now use these as their base class.

And you can now ignore R2 event name changes and name these events to whatever you want.

Caveat – i have found a couple interesting bugs in the 2019r1.1 IDE doing this

When moving some control subclasses into a folder insances on a layout may “lose” their super especially if neither of the modules has a compat flag set.
So you get an error like

CustomTextField.Super
Can't find a type with this name. Did you mean class For2019r1andEarlier.MyTextFieldBase?
MyTextFieldBase

Fixing this amounts to enabling one of the compat flags and then going to the super field of the affected control and putting the cursor in the super field and pressing enter

Here’s an example  that shows how I’ve set things up

Sort like Finder

Sometimes you want to be able to sort things in the same fashion as the Finder. This method when used as the comparison function for the Arry.Sort( delegate) form of sorting will do that

Public Function CompareLikeFinder(firstString as string, secondString as String) as integer
  #If targetMacOS
    // // 
    // // typedef NS_CLOSED_ENUM(NSInteger, NSComparisonResult) {
    // // NSOrderedAscending = -1L,
    // // NSOrderedSame,
    // // NSOrderedDescending
    // // };
    // 
    // // this gives us "Finder like" comparisons
    
    // -[NSString localizedStandardCompare:].
    Declare Function localizedStandardCompare Lib "Foundation" selector "localizedStandardCompare:" (string1 As CFStringRef, string2 As CFStringRef) As Integer
    Return localizedStandardCompare(firstString, secondString)
    
    
  #Else
    
    Return StrComp(firstString, secondString, REALbasic.StrCompLexical)
    
  #EndIf
  
  
  
End Function

So with code like

Dim strings() As String
strings.append "file10"
strings.append "file1"
strings.append "file2"
strings.append "file11"

Strings.sort( AddressOf CompareLikeFinder )

You will get

file1
file2
file10
file11

instead of a strictly lexically ordered list

Is full keyboard access on ?

Sometimes its useful to be able to be able to tell your user if they need to enable full keyboard access

You can tell if its enabled with this method

Public Function FullKeyboardAccessEnabled() as Boolean
  #If TargetMacOS
    Try
      Declare Function NSClassFromString Lib "Cocoa" (name As CFStringRef) As Ptr
      Declare Function GetSharedApplication Lib "AppKit" Selector "sharedApplication" (target As Ptr) As Ptr
      Declare Function IsFullKeyboardAccessEnabled Lib "AppKit" Selector "isFullKeyboardAccessEnabled" (target As Ptr) As Boolean
      
      Dim AppClass As Ptr = NSClassFromString("NSApplication")
      If AppClass <> Nil Then
        Dim AppObject As Ptr = GetSharedApplication(AppClass)
        If AppObject <> Nil Then
          Return IsFullKeyboardAccessEnabled(AppObject)
        End If
      End If
    Catch Err As RuntimeException
      Return False
    End Try
  #endif
  
  
  // windows & linux can tab to EVERY control anyway
  return true
End Function

Opting in or out of Windows AutoTabbing

macOS adopted an interesting behaviour some time ago where when you opened a new window in an application. It would default to opening a new tab in the window instead of opening a new window.

If your application could not deal with this and the user had the preference in System Preferences > Dock > Prefer Tabs when opening documents set to Always then it could cause your app issues.

You can opt out of this behaviour in your app’s open event.

Public Sub AppAutoTabbing(optIn as boolean)
  #if not TargetMacOS
    #pragma unused optIn
  #endif
  
  #if TargetMacOS
    
    Declare Function NSClassFromString Lib "Cocoa" (name As CFStringRef) As Ptr
Dim nsWindowClass As ptr = NSClassFromString( "NSWindow" ) 

if nsWindowClass = nil then
  // msgbox CurrentMethodName + " nsWindowClass is Nil"
  break
  return
end if

declare sub EnableTabGrouping lib "AppKit"selector "setAllowsAutomaticWindowTabbing:" ( classPtr as Ptr , enableDisable as Boolean ) 

EnableTabGrouping( nsWindowClass, optIn )
    
  #endif
  
End Sub

Switch the window type at runtime

I’ve seen this question come up a couple times recently. And while it would be nice to do you cant.

The frame type is required to be set when the window is constructed. And there’s no constructor on a window that allows you to do something like

dim w as new Window(frametype)

So how can you have something like a document window on macOS and a movable modal on Windows ?

With a little inventiveness and some work.

First – make a container control that will be the windows content. All your common ui elements go in here. This is the common UI.

Then create a new window for macOS and set its frame type to whatever you need on macOS.

And one for Windows and Linux if needed.

One each embed, at design time, an instance of the container control you just created. Any non-common ui can be added directly to the windows themselves.

Now at runtime when you need an instance of this window you can use conditional compilation to make sure you get the right kind of window.

I’ve put together a small sample

Showing a file in Windows Explorer

No. Not IE.

// there are issues with this style
//  1. you can only select one file
//  2. If folder is already opened, it doesn't select the file
Declare Function ShellExecuteW lib "shell32" (hwnd as Integer, lpOperation as WString, lpFile as WString, lpParameters as WString, lpDirectory as Integer, nShowCmnd as Integer) as Integer

Dim err as Integer
Dim param As String

param = "/select, """ + f.AbsolutePath + """"

err = ShellExecuteW(Window(0).WinHWND, "Open", "explorer", param, 0, 1)

EDIT – a better version has been written by Julian Samphire

He’s given me permission to post it here. Note there are two methods. One takes an array of file paths. The second overloads things to accept paramarry that then calls into the version that takes an array

Public Function ShowSelectedInExplorer(path As String, files() As String) as Int32
  Declare Function CoInitialize Lib "Ole32.dll" Alias "CoInitialize" (pvReserved As Integer) As Int32
  Declare Function ILCreateFromPathW Lib "Shell32.dll" Alias "ILCreateFromPathW" (pszPath As WString) As Ptr
  Declare Function SHOpenFolderAndSelectItems Lib "Shell32.dll" Alias "SHOpenFolderAndSelectItems" (pidlFolder As Ptr, cidl As UInt32, apidl As Ptr, dwFlags As UInt32) As Int32
  Declare Sub ILFree Lib "Shell32.dll" Alias "ILFree" (pidl As Ptr)
  
  Call CoInitialize(0)
  
  Dim pidl As Ptr = ILCreateFromPathW(path)
  If pidl = Nil Then 
    Return 0 'If path wasn't found then return
  End If
  
  Dim mb As New MemoryBlock(COM.SIZEOF_PTR * (files.Ubound + 1))
  
  'Get a pidl for each file and store it
  For i As Integer = 0 To files.Ubound
    mb.Ptr(i * COM.SIZEOF_PTR) = ILCreateFromPathW(files(i))
  Next
  
  Dim ok As Int32 = SHOpenFolderAndSelectItems(pidl, files.Ubound + 1, mb, 0)
  
  ILFree(pidl)
  
  Return ok
End Function


Public Function ShowSelectedInExplorer(path As String, ParamArray files As String) as Int32
  'Build an array from the param array and pass it back to ShowSelectedInExplorer so we can support both methods
  Dim arrayOfFiles() As String
  For Each f As String In files
    arrayOfFiles.Append(f)
  Next
  Return ShowSelectedInExplorer(path, arrayOfFiles)
End Function

Showing a file in the Finder

This has probably been posted in the forums. Its just not always easy to find.

Declare Function NSClassFromString Lib "Cocoa" (name As CFStringRef) As Ptr
Declare Function sharedWorkspace Lib "AppKit" selector "sharedWorkspace" ( obj As ptr ) As ptr
Declare Function selectFile Lib "AppKit" selector "selectFile:inFileViewerRootedAtPath:" ( obj As ptr, fPath As CFStringRef, rootFullPath As CFStringRef ) As Boolean
Dim workspace As ptr = sharedWorkspace( NSClassFromString( "NSWorkspace" ) )

Call selectFile( workspace, f.NativePath, "")

Debugging tip

Sometime when you’re debugging your application you run into a situation where you get funky behaviour.

You might do something like Javier mentioned in his recent blog post on Xojo’s blog :

Dim ASource() As Integer = Array(1,2,3,4,5,6,7,8)
Dim ATarget() As Integer
ATarget = ASource

And, as he noted, you cant figure out why when you change one array you also appear to alter the other. This can also happen with other reference types as I noted in other posts.

For instance I’ve seen

Dim d As New Date
Dim d1 As date
d1 = d

d1.month = 2

and then the question of “why did d change?” arises

Again this has been covered before and it has to do with both arrays and dates, as well as many other type, being reference types.

One way to see that in fact these are the same object is not well documented in the Xojo documentation. Its buried in the Debugging pane of the preferences – Show Object IDs in variable lists.

Once you enable this setting what you see in the debugger pane makes it much easier to see when you have two references to the same object.

When viewing the following code

Dim ASource() As Integer = Array(1,2,3,4,5,6,7,8)
Dim ATarget() As Integer
ATarget = ASource

You can clearly see that ATarget and ASource have the same objectID. In Xojo’s runtime this means they are the same object – objectsID’s are unique to every instance and the only way you get two objectID’s that are the same is when two references refer to the same object.

I’d recommend always turning this setting on when debugging.