21 August 2019

Unicode controls

In the passed years I’m frequently asked for ‘Unicode support’ in GFA-BASIC 32. The issue here is that GB is an ANSI programming language; the IDE accepts only characters in the range from 0 to 255 and the string functions assume one byte per character. All commands and functions that accept a string parameter only take ANSI strings. However, it is possible to create UNICODE controls and let the user input text in the user’s locale setting and then retrieve the wide character text from the controls. To process the retrieved text the application will most likely use Windows API wide-string functions.

A few notes
An introduction to Unicode strings can be found in a previous post: Ansi and Unicode.

This blog post will discuss the use of Unicode (or wide character) controls on a GFA-BASIC 32 form, specifically on a Dialog form. The code is discussed in bits and pieces, but the code for the entire example program can be downloaded here.

Declaring wide API functions
When an application wants to use wide character controls the Ocx property and sub-event system cannot be used any longer. In addition, a dialog definition has to be set up in code, because the form-editor can no longer be used either. The controls have to be created and handled using Windows W-API functions. Windows defines both ANSI and Unicode variants for API functions that take a string parameter. Many of the ANSI APIs are built-in in GFA-BASIC, but the W variants are missing and have to be declared explicitly. Two wide char APIs an application will definitely use are CreateWindowExW and SendMessageW. They have to be declared explicitly (abbreviated):

Declare Function CreateWindowExW Lib "user32" (ByVal dwExStyle As Long,
Declare Function SendMessageW Lib "user32" (ByVal hWnd As Handle, …

Other possible declares are lstcmpW, lstrcmpiW, CharUpperW, and CharLowerW. To draw Unicode text on the screen the application needs to declare TextOutW and/or DrawTextW, etc.

  • Recommended: A full set of wide-string functions can be found in the Shell Lightweight Utility functions (SHLWAPI) DLL. The Include library does not provide an include file with Wide function declarations though!

Defining controls
We cannot use any predefined control and we cannot use the Control command to create a wide character control. We can however create a procedure ControlW that allows an easy translation of Control statements to Unicode controls. Because we will use a W variant of the Control command, an easy way to add controls is by using a dialog box (which is a Form). This also allows us to use an external dialog box editor. The following piece of code is created using ResHacker, a GUI utility that provides the ability to create a dialog box definition. After copying and pasting the definition into the GB editor the command Control is replaced by ControlW. Note that ResHacker produces a dialog definition with dialog base units rather than pixels. Also, the dimension of the controls may need some editing once the dialog is used in GB. The WS_CHILD | WS_VISIBLE styles can be removed as well.

DlgBase Unit
Dlg 3D On
Dialog # 1, 0, 20, 261, 140, "Controls", WS_SYSMENU | WS_CAPTION
  ControlW "&OK", 1, "BUTTON", BS_DEFPUSHBUTTON | 
WS_CHILD | WS_VISIBLE | WS_TABSTOP,
130, 97, 50, 11 ControlW "&Cancel", 2, "BUTTON", BS_PUSHBUTTON |
WS_CHILD | WS_VISIBLE | WS_TABSTOP,
187, 97, 50, 11 ControlW "Checkbox", 10, "BUTTON", BS_AUTOCHECKBOX |
WS_CHILD | WS_VISIBLE | WS_TABSTOP,
7, 8, 60, 14 ControlW "Group", 0, "BUTTON", BS_GROUPBOX |
WS_CHILD | WS_VISIBLE,
7, 23, 59, 47, WS_EX_TRANSPARENT ControlW "Radio 1", 12, "BUTTON", BS_RADIOBUTTON |
WS_CHILD | WS_VISIBLE | WS_TABSTOP,
12, 36, 43, 14 ControlW "Radio 2", 13, "BUTTON", BS_RADIOBUTTON |
WS_CHILD | WS_VISIBLE | WS_TABSTOP,
12, 51, 43, 14 ControlW "Trackbar", 15, "msctls_trackbar32", TBS_HORZ |
WS_CHILD | WS_VISIBLE | WS_TABSTOP,
7, 77, 60, 18 ControlW "Insert Text:", 0, "STATIC", SS_LEFT |
WS_CHILD | WS_VISIBLE | WS_GROUP,
82, 10, 45, 10 ControlW "", 17, "EDIT", ES_LEFT | ES_MULTILINE |
WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP,
122, 7, 116, 14 EndDialog

The ControlW procedure creates the wide control. The string input parameters are ANSI strings that are converted to Unicode before the CreateWindowsExW is invoked. Since the dialog definition uses dialog box units the Dlg Base Units command is added. This command initializes a few global variables in the runtime. The ControlW procedure tests if Dlg Base Units is used and if it is used converts the coordinates from dialog box units to pixels.

Proc ControlW(text$, id%, class$, style%, x%, y%, w%, h%, Optional exstyle%)
  Local hWnd As Handle, pText As Long
  If Len(text$) Then text$ = Wide(text$) : pText = V:text$
  class$ = Wide(class$)
  style% |= WS_CHILD | WS_VISIBLE
  If {$180B5F70} %& 1    ' DlgBase Units?
    x% = MulDiv(x%, {$180B5E98}, 4)
    y% = MulDiv(y%, {$180B5E94}, 8)
    w% = MulDiv(w%, {$180B5E98}, 4)
    h% = MulDiv(h%, {$180B5E94}, 8)
  EndIf
  hWnd = CreateWindowExW(exstyle%, V:class$, pText, style%, x%, y%, w%, h%, Me.hWnd, id%, _INSTANCE, 0)
  If hWnd _
    SendMessageW(hWnd, WM_SETFONT, Me.Font._hFont, 1)
EndProc

The controls are created using pure Windows API, this also means that the controls have to be initialized and modified by sending messages. You will need proper documentation to know which message and how to send it to the controls. The controls are not OCX controls and don’t respond to notification messages through an event sub. The notification messages from the controls come either in WM_COMMAND or WM_NOTIFY message. The application needs to process these messages the ‘API-way’.

Notes on using a dialog box
An advantage of using a Dialog form is the presence of properties and event-subs. To respond to control-messages a Dlg_n_Message sub is all that is needed. A disadvantage of using a Dialog form is the lack of Unicode support for the title of the ANSI-based dialog box. One solution could be to add an informative picture along the top (caption) of the dialog form. This would require a simple LoadPicture and PaintPicture sequence of commands.
In addition, ANSI and Unicode controls can not be used together, that would break the Tab-key navigation. Even worse, the navigation with Unicode controls differs from the navigation with ANSI controls. This means commands like Sleep and PeekEvent will mess up the key-navigation. A work-around is to trap the Tab- and arrow keys in the Screen_KeyPreview event sub and call IsDialogMessage ourselves.

Sub Screen_KeyPreview(hWnd%, uMsg%, wParam%, lParam%, Cancel?)
  Dim msg As MSG
  If GetForegroundWindow() = Dlg_1.hWnd
    msg.hwnd = hWnd%
    msg.MessageVar = uMsg%
    msg.wParam = wParam%
    msg.lParam = lParam%
    Cancel? = IsDialogMessage(Dlg_1.hWnd, msg) == 1
  EndIf
EndSub

Cancel is set to True when IsDialogMessage handled the key. This prevents the handling of the navigation key in commands like Sleep and PeekEvent.
IsDialogMessage is always called as part of the message handling commands and IsDialogMessage processes the key when the form contains at least one control (might be a toolbar or statusbar). It seems the GB application ‘eats’ the keypresses if you’re not aware of this behavior.

Another issue is the way the focus is handled in a form with controls. The application should always explicitly set the focus to a control before entering the main message loop. If it doesn’t the focus might not be set correctly when a navigation key is pressed or when the application is reactivated.

Processing Unicode strings
The ControlW custom procedure takes an ANSI string for the control text. However, the program needs to set the controls text using Unicode instead. Normally, a program assigns hard coded text to a control, but the text in the IDE is limited to ANSI characters. Somehow the text must be obtained from a Unicode source that can be used as literal strings. Because it is (almost) impossible to specify Unicode strings in code directly, strings have to be obtained from an external source. This is possible with the use of an editor that can save Unicode strings. For this example I used NotePad2 that can save Unicode strings by setting the Encoding in the File menu to Unicode. In the GB code I defined constants with the index of the strings after they have been loaded into an array. These lines can be found at the start of the example program:

Dim T$$()   ' storage for UNICODE strings
Enum wsHello, wsGFABASIC
LoadWStrings("unicode.txt", T$$())

The procedure LoadWStrings loads the Unicode text lines into the array T$$(). The double $ is used to indicate that the string array variable contains wide character strings.

Now, after the dialog box has been created, but before it is displayed, the text of the wide controls can be modified using a string from T$$(). For this to happen the program includes a SetW procedure which assigns a Unicode string to a window. In the same style a GetW function returns a Unicode string from a window.

Function GetW(ByVal hwnd As Handle, Optional InclTerm As Bool = False) As String
  Local size As Long, sBuf As String
  size = SendMessageW(hwnd, WM_GETTEXTLENGTH, 0, 0)
  If size
    size++     ' also obtain the terminating null bytes
    sBuf = String(size Mul 2, #0)
    SendMessageW(hwnd, WM_GETTEXT, size, V:sBuf)
    GetW = InclTerm ? sBuf : Left(sBuf, Len(sBuf) - 2)
  EndIf
EndFunc

Proc SetW(ByVal hwnd As Handle, ByVal wTxt As String)
  If Right(wTxt, 2) != #0#0 Then wTxt += #0#0
  SendMessageW(hwnd, WM_SETTEXT, 0, V:wTxt)
EndProc

For instance, to set the text of the wide EDIT control in the dialog box to Hello:

SetW Dlg(1, 17), T$$(wsHello)

By default GetW returns a Unicode without the terminating two null bytes. However, if a Unicode string is later to be passed to a Windows function, the string is expected to end with two terminating null bytes. So, it depends on the purpose of the string whether or not the string should include the terminating zeros. GetW can return the Unicode string with the terminating bytes as well.
As an example the program also provides a way to compare Unicode strings using Windows API functions:

Function StrCmpW(ByVal str1 As String, ByVal str2 As String, 
Optional ignorecase As Bool) As Bool If Right(str1, 2) != #0#0 Then str1 += #0#0 If Right(str2, 2) != #0#0 Then str2 += #0#0 If ignorecase StrCmpW = lstrcmpiW(str1, str2) == 0 Else StrCmpW = lstrcmpW(str1, str2) == 0 EndIf EndFunc

The function StrCmpW is wrapper around the declared lstrcmpiW and lstrcmpW APIs. Before these APIs are invoked the strings are tested for the two terminating null bytes. If they are missing the strings are modified. For example, to test if the edit-control holds the word ‘hello’ the following code might be used:

wTxt = GetW(Dlg(1, 17))
If StrCmpW(wTxt, T$$(wsHello), True)
  MsgBox "Edit control specified hello"
EndIf

Summary
A GFA-BASIC application can provide Form-based Unicode controls. The IDE does not allow Unicode literal strings so they must come from an external source. In addition, many other commands like MsgBox, Dlg Open/Save require ANSI strings, so the application must use the appropriate wide Windows API functions. To be fully Unicode the application should be created using wide character API functions entirely.

No comments:

Post a Comment