Login or Sign Up to become a member!
LessThanDot Sit Logo

LessThanDot

Data Management

Less Than Dot is a community of passionate IT professionals and enthusiasts dedicated to sharing technical knowledge, experience, and assistance. Inside you will find reference materials, interesting technical discussions, and expert tips and commentary. Once you register for an account you will have immediate access to the forums and all past articles and commentaries.

LTD Social Sitings

Lessthandot twitter Lessthandot Linkedin Lessthandot friendfeed Lessthandot facebook Lessthandot rss

Note: Watch for social icons on posts by your favorite authors to follow their postings on these and other social sites.

Your profile

    Search

    XML Feeds

    Google Ads

    « SQL Server Auditing: Managing Your AuditsPerformance Impacts of Unicode, Equals vs LIKE, and Partially Filled Fixed Width »
    comments

    Recently I had to deal with the problem of displaying and saving Unicode data in a Visual FoxPro form. As we all know, Visual FoxPro does not support Unicode, so this was quite a challenge for me. I would never have solved this problem by myself if Gregory Adam from the UniversalThread would not lend me his generous support and practically solved the problem of saving for me. I want to show the full solution we came to together as this may be a very important topic for the remaining Visual FoxPro developers (and because Koen asked me to write this blog post).

    Ok, we all know the classical and very comprehensive article on this topic written by Rick Strahl Using Unicode in Visual FoxPro Web and Desktop Applications. So, I started from this article. Our application uses SQL Server to hold data and uses sql pass thru technique to send information from and to SQL Server.

    Now, since I am describing the solution I used for the particular application, I will start with the SQL Server table I was using. That table was named Prefs_sl and it was a flat table with 1 record containing all preferences for a sales point. Because of its flat nature it was badly normalized. Say, it had 11 language fields language00 - language10 (nvarchar(100)) to hold name of the language the Sales Point can display and 11 image lngImage00-lngImage10 (varbinary(max)) fields to hold the image of the country flag. So, this was the table I was working with on the SQL Server side.

    Now, I wanted to display these 11 languages with their corresponding flags on the form, so the logical choice was to create a class with the textbox for the unicode text and image for the image flag and then instantiate this class on the form.

    The first question was what to chose for displaying unicode data? Rick mentioned MS Edit Box control ActiveX, but I didn't find it in the list of my installed ActiveX. But I found MS Forms2 TextBox ActiveX control and since I also remembered this thread on foxite forum, I decided to try this ActiveX control.
    The first problem I encountered with this ActiveX as that it didn't show up until I clicked on it when I was running the form. Luckily I found the following solution - invoke SetFocus of this control (I called it txtLanguage in my class) and then it was displayed correctly. I am not sure if my solution is the best to handle this ActiveX behavior quirks, but hey, it works.

    Ok, so far so good. Now, following Rick's article I used the following in my form to get the data in the binary format:

    1. LOCAL lcSQL
    2. TEXT TO lcSQL noshow
    3. SELECT pri_key      
    4.       ,defltlang
    5.       ,cast(language00 as varbinary(100)) as language00
    6.       ,lngimage00      
    7.       ,cast(language01 as varbinary(100)) as language01
    8.       ,lngimage01
    9.       ,cast(language02 as varbinary(100)) as language02
    10.       ,lngimage02
    11.       ,cast(language03 as varbinary(100)) as language03
    12.       ,lngimage03
    13.       ,cast(language04 as varbinary(100)) as language04
    14.       ,lngimage04
    15.       ,cast(language05 as varbinary(100)) as language05
    16.       ,lngimage05
    17.       ,cast(language06 as varbinary(100)) as language06
    18.       ,lngimage06
    19.       ,cast(language07 as varbinary(100)) as language07
    20.       ,lngimage07
    21.       ,cast(language08 as varbinary(100)) as language08
    22.       ,lngimage08
    23.       ,cast(language09 as varbinary(100)) as language09
    24.       ,lngimage09
    25.       ,cast(language10 as varbinary(100)) as language10
    26.       ,lngimage10      
    27.   FROM dbo.prefs_sl
    28. endtext  
    29. mysqlexec(m.lcSQL, 'Prefs_sl', program())

    mysqlexec here is a wrapper for the sqlexec function.

    Now I found the first discrepancy with Rick's article - I found that I don't need any conversion after that, I can assign the field's content to the text property of this ActiveX (or ControlSource property) and the control correctly displays the unicode data!

    I was happy - the first part of displaying data turned out to be quite easy!

    Here is the code I used to display the data:

    1. FOR EACH loObject IN thisform.Objects foxobject
    2.    IF loObject.Baseclass = 'Container'
    3.       loObject.LanguageField = SUBSTR(loObject.name,4,LEN(loObject.name))
    4.       loObject.ImageField = 'lngImage' + RIGHT(loObject.Name,2)
    5.    ENDIF
    6. next  
    7.  
    8. thisform.cntLanguage01.SetFocus()

    And in the container itself the following code in the LanguageField assign method:

    1. this.lblLanguage.caption = this.lblLanguage.caption + ' ' + transform(val(right(tLanguageField,2)))
    2.   this.txtLanguage.Enabled = .t.
    3.   this.txtLanguage.TabStop = .t.  
    4.   this.lProgrammaticChange = .t.
    5.   this.txtLanguage.text = evaluate('prefs_sl.' + tLanguageField)
    6.   this.lProgrammaticChange = .f.

    Everything was good so far. However, I haven't expected how hard (for me) it will be the 'saving' data part. I was spending days trying various ideas from Rick's article and was still unable to achieve the desired result. I was about to throw all I had away and try to switch to ADO for data retrieval, but luckily Gregory Adam helped me here and published a working sample.

    First of all, we need to make sure there is no UT8 translation to the current code page. For this we need to do the following

    1. COMPROP(this,'UTF8',1)

    In the txtLanguage Init method.

    We also need the following functions:

    1. && StringConversion
    2. && Gregory Adam 2011
    3. *===============================================================================
    4. #define true    .T.
    5. #define false   .F.
    6.  
    7. *_______________________________________________________________________________
    8. function StringToUTF8(utf8Out, stringIn, codepageIn)
    9.    
    10.     local success
    11.     success = true
    12.    
    13.     do case
    14.     case !m.success
    15.    
    16.     case !StringToUTF16(@m.utf8Out, m.stringIn, m.codepageIn)
    17.         assert false
    18.         success = false
    19.    
    20.     case !StringUTF16ToUTF8(@m.utf8Out, m.utf8Out)
    21.         assert false
    22.         success = false
    23.    
    24.     endcase
    25.    
    26.     return m.success
    27.    
    28. endfunc
    29. *_______________________________________________________________________________
    30.  
    31.  
    32. *_______________________________________________________________________________
    33. *_______________________________________________________________________________
    34. #define CP_ACP                  0
    35. #define CP_MACCP                2
    36. #define CP_OEMCP                1
    37. #define CP_SYMBOL               42
    38. #define CP_THREAD_ACP           3
    39. #define CP_UTF7                 65000
    40. #define CP_UTF8                 65001
    41. #define MB_PRECOMPOSED          0x1
    42. #define MB_COMPOSITE            0x2
    43. #define MB_USEGLYPHCHARS        0x4
    44. #define MB_ERR_INVALID_CHARS    0x8
    45.  
    46. #define WC_DEFAULTCHAR          0x00000040
    47. #define WC_ERR_INVALID_CHARS    0x00000080
    48. #define WC_NO_BEST_FIT_CHARS    0x00000400
    49. *_______________________________________________________________________________
    50. function StringToUTF16(utf16Out, stringIn, codepageIn)
    51.  
    52.     local success
    53.     success = true
    54.    
    55.     do case
    56.     case !m.success
    57.    
    58.     case empty(len(m.stringIn))
    59.         utf16Out = ''
    60.        
    61.     otherwise
    62.         local lpWideCharStr, result
    63.         lpWideCharStr = space(len(m.stringIn)*2)
    64.    
    65.         result = MultiByteToWideChar( ;
    66.                     evl(m.codepageIn, cpcurrent()), ;
    67.                     MB_ERR_INVALID_CHARS, ;
    68.                     @m.stringIn, ;
    69.                     len(m.stringIn), ;
    70.                     @m.lpWideCharStr, ;
    71.                     len(m.lpWideCharStr) ;
    72.                 )
    73.            
    74.         do case
    75.         case !m.success
    76.        
    77.         case empty(m.result)
    78.             assert false
    79.             success = false
    80.        
    81.         otherwise
    82.             utf16Out = left(m.lpWideCharStr, m.result * 2)
    83.        
    84.         endcase
    85.        
    86.     endcase
    87.    
    88.     return m.success
    89.    
    90.    
    91. endfunc
    92. *_______________________________________________________________________________
    93. function StringUTF16ToUTF8(utf8Out, utf16In)
    94.  
    95.     local success
    96.     success = true
    97.    
    98.     do case
    99.     case !m.success
    100.    
    101.     case empty(len(m.utf16In))
    102.         utf8Out = ''
    103.        
    104.     otherwise
    105.    
    106.         local lpMultiByteStr, lpUsedDefaultChar, result
    107.         lpMultiByteStr = space(len(m.utf16In) * 2)
    108.         lpUsedDefaultChar = 0
    109.        
    110.         result = WideCharToMultiByte( ;
    111.                     CP_UTF8, ;
    112.                     WC_ERR_INVALID_CHARS, ;
    113.                     @m.utf16In, ;
    114.                     len(m.utf16In)/2, ;
    115.                     @m.lpMultiByteStr, ;
    116.                     len(m.lpMultiByteStr), ;
    117.                     null, ;
    118.                     @m.lpUsedDefaultChar ;
    119.                 )
    120.        
    121.         do case
    122.         case !m.success
    123.        
    124.         case empty(m.result)           
    125.             assert false
    126.             success = false
    127.            
    128.            
    129.         otherwise
    130.             utf8Out = left(m.lpMultiByteStr, m.result)
    131.        
    132.         endcase
    133.     endcase
    134.    
    135.     return m.success
    136.    
    137.    
    138. endfunc
    139. *_______________________________________________________________________________
    140. function StringUTF8ToUTF16(utf16Out, uft8In)
    141.  
    142.     return StringToUTF16(@m.utf16Out, uft8In, CP_UTF8)
    143.    
    144. endfunc
    145. *_______________________________________________________________________________
    146.  
    147. function MultiByteToWideChar
    148.     lparameters codepage, ;
    149.                 dwFlags, ;
    150.                 lpMultiByteStr, ;
    151.                 cbMultiByte, ;
    152.                 lpWideCharStr, ;
    153.                 cchWideChar
    154.  
    155.     local success
    156.     success = true
    157.  
    158.     local result
    159.    
    160.     do case
    161.     case !m.success
    162.    
    163.     otherwise
    164.         try
    165.             declare integer MultiByteToWideChar in Kernel32.dll ;
    166.                 long    codepage, ;
    167.                 long    dwFlags, ;
    168.                 string@ lpMultiByteStr, ;
    169.                 integer cbMultiByte, ;
    170.                 string@ lpWideCharStr, ;
    171.                 integer cchWideChar
    172.        
    173.             result = MultiByteToWideChar( ;
    174.                     m.codepage, ;
    175.                     m.dwFlags, ;
    176.                     @m.lpMultiByteStr, ;
    177.                     m.cbMultiByte, ;
    178.                     @m.lpWideCharStr, ;
    179.                     m.cchWideChar ;
    180.                 )
    181.         catch
    182.             assert false
    183.             success = false
    184.            
    185.         endtry
    186.     endcase
    187.    
    188.     return iif(m.success, m.result, 0)
    189.    
    190. endfunc
    191. *_______________________________________________________________________________
    192. function WideCharToMultiByte
    193.     lparameters codepage, ;
    194.                 dwFlags, ;
    195.                 lpWideCharStr, ;
    196.                 cchWideChar, ;
    197.                 lpMultiByteStr, ;
    198.                 cbMultiByte, ;
    199.                 lpDefaultChar, ;
    200.                 lpUsedDefaultChar
    201.  
    202.     local success
    203.     success = true
    204.  
    205.     local result
    206.    
    207.     do case
    208.     case !m.success
    209.    
    210.     otherwise
    211.         try
    212.             declare integer WideCharToMultiByte in Kernel32.dll ;
    213.                 long    codepage, ;
    214.                 long    dwFlags, ;
    215.                 string@ lpWideCharStr, ;
    216.                 integer cchWideChar, ;
    217.                 string@ lpMultiByteStr, ;
    218.                 integer cbMultiByte, ;
    219.                 string  lpDefaultChar, ;
    220.                 integer @lpUsedDefaultChar
    221.        
    222.             result = WideCharToMultiByte ( ;
    223.                     m.codepage, ;
    224.                     m.dwFlags, ;
    225.                     @m.lpWideCharStr, ;
    226.                     m.cchWideChar, ;
    227.                     @m.lpMultiByteStr, ;
    228.                     m.cbMultiByte, ;
    229.                     m.lpDefaultChar, ;
    230.                     @m.lpUsedDefaultChar;
    231.                 )
    232.         catch
    233.             assert false
    234.             success = false
    235.            
    236.         endtry
    237.     endcase
    238.    
    239.     return iif(m.success, m.result, 0)
    240.    
    241. endfunc

    and then in the Change event of the ActiveX textbox I added the following code

    1. IF this.parent.lProgrammaticChange
    2.     RETURN
    3. ENDIF    
    4. if thisform.IsChanged OR (not this.CurrentValue == this.Text)
    5.    thisform.IsChanged = .t.
    6.    local utf8, utf16Out
    7.    utf8 = this.text
    8.    if not empty(utf8)
    9.       = StringUtf8ToUTF16(@m.utf16Out, m.utf8)
    10.       replace (this.parent.LanguageField) with utf16Out
    11.    else
    12.       replace (this.parent.LanguageField) with ''
    13.    endif
    14. endif

    and finally in the Save method of the form

    1. IF THISFORM.IsChanged
    2.   LOCAL lcSQL
    3.  
    4.   TEXT TO lcSQL noshow
    5.      UPDATE dbo.prefs_sl
    6.      SET defltlang = ?prefs_sl.DefltLang,
    7.      language00 = cast(?prefs_sl.language00 as nvarchar(100)),
    8.      lngimage00 = ?prefs_sl.lngimage00,
    9.      language01 = cast(?prefs_sl.language01 as nvarchar(100)),
    10.      lngimage01 = ?prefs_sl.lngimage01,
    11.      language02 = cast(?prefs_sl.language02 as nvarchar(100)),
    12.      lngimage02 = ?prefs_sl.lngimage02,
    13.      language03 = cast(?prefs_sl.language03 as nvarchar(100)),
    14.      lngimage03 = ?prefs_sl.lngimage03,
    15.      language04 = cast(?prefs_sl.language04 as nvarchar(100)),
    16.      lngimage04 = ?prefs_sl.lngimage04,
    17.      language05 = cast(?prefs_sl.language05 as nvarchar(100)),
    18.      lngimage05 = ?prefs_sl.lngimage05,
    19.      language06 = cast(?prefs_sl.language06 as nvarchar(100)),
    20.      lngimage06 = ?prefs_sl.lngimage06,
    21.      language07 = cast(?prefs_sl.language07 as nvarchar(100)),
    22.      lngimage07 = ?prefs_sl.lngimage07,
    23.      language08 = cast(?prefs_sl.language08 as nvarchar(100)),
    24.      lngimage08 = ?prefs_sl.lngimage08,
    25.      language09 = cast(?prefs_sl.language09 as nvarchar(100)),
    26.      lngimage09 = ?prefs_sl.lngimage09,
    27.      language10 = cast(?prefs_sl.language10 as nvarchar(100)),
    28.      lngimage10 = ?prefs_sl.lngimage10
    29.      where pri_key = ?prefs_sl.pri_key
    30.   ENDTEXT
    31.  RETURN mySQLExec(m.lcSQL, '',PROGRAM())
    32. ENDIF

    As we can see, we need to convert data back from binary to nvarchar.
    With all this code in place, we display the unicode data and save them back to SQL Server.

    A big thanks to Gregory for helping me with the code - I don't know where I would be without his help.

    Olaf Doschke showed another way which is even simpler than implemented solution and in accordance to the original Rick's article.

    In the form's Load we need the following:

    1. Sys(987,.F.)
    2. Sys(3101,65001)

    Then, after getting binary data from SQL Server the same way as I show in this blog, we still need to use createbinary, e.g.

    this.txtLanguage.text = createbinary(prefs_sl.Language00)

    We don't want to use COMPROP now for the ActiveX.

    and then, saving data, we need to follow Rick's steps:

    1. pcSavedText1 = Strconv(This.Text,12)
    2.  
    3. *** Must explicitly force to binary – can also use CAST in 9.0
    4. pcSavedText1 = CREATEBINARY(pcSavedText1)

    and then convert this value back to nvarchar(max) when saving with sqlexec.

    This is how the form with many languages looked like

    ---------------------------
    I need to also tell, that after I finished the form and was happy to show it to my colleagues, I was up to a big disappointment. It turned out we are not going to support unicode (our main application is not up to support it yet) and so I will be re-doing this form after we will agree on the tables' structures (they also are going to change). So, what I showed in this blog post we're not going to use at present in production.

    UPDATE. My colleague also re-designed prefs_sl table to one EAV Settings table. We know that EAV design has its own problems, but for saving various application level settings it can be used. I already re-designed the forms we're going to use.

    But in any case, it was a very important exercise and I hope it will help other people who need to display and save Unicode data in their Visual FoxPro applications.

    About the Author

    Naomi Nosonovsky, Senior Software Developer, has more than 15 years of enterprise experience in analysis, design, development and implementation of high-performance client/server data management solutions. She is a Personality of the Year for 2008, 2009, 2010 and 2011 at www.universalthread.com in .NET, SQL Server & VFP categories. She is also an All Star contributor/MCC Community Contributor at forums.asp.net and MSDN T-SQL forum.
    Social SitingsFacebookLinkedIndeliciousLTD RSS Feed
    849 views
    Instapaper

    3 comments

    Comment from: Koen [Visitor] Email
    Koen Naomi,
    Thanks for sharing, this will surely make tha complex matter more accesible than the lengthy discussion in FT.
    Special thanks to Gregory!
    01/01/12 @ 12:32
    Comment from: khubaib Ahmad Khalid [Visitor]
    khubaib Ahmad Khalid good, If you create a project and send in download section, more helpful.
    thanks
    01/03/12 @ 06:05
    Comment from: Adam [Visitor] Email
    Adam I'm trying to display some Unicode data stored in dbf fields/memo fields on a form. I'm using only fox native DBF. All the examples relating to Unicode are SQL so it's complicated for everyone.

    Could any of you please come up with a simpler way to do this. Some simpler codes that would display a form with a textbox and with a native dbf (not SQL).

    I think a class/function might be a good method. After achieving that goal anyone can get used with the idea and work his way in dealing with zillions of languages in a form.

    If we all can find a simpler solution for this; then VFP lives again.

    Thanks in advance.

    adam7171@gmail.com
    04/22/12 @ 10:25

    Leave a comment


    Your email address will not be revealed on this site.

    Your URL will be displayed.
    (Line breaks become <br />)
    (Name, email & website)
    (Allow users to contact you through a message form (your email will not be revealed.)