Ошибки, связанные с локализацией вообще не редкость. Думаю, каждый натыкался на случай, когда копируешь код из справки, а он не работает. А все потому, что в справке стоит точка, а по условиям локализации нужна запятая. Или запятая вместо точки с запятой.
Но еще неприятнее, когда нужен макрос, который должен работать в любой локализации. Здесь уже не обойдешься простой заменой символа в коде. Или как минимум такую замену нужно делать "на лету", во время исполнения макроса. Рассмотрим несколько примеров.
Десятичный разделитель
Предположим, имеется небрежно составленный файл Excel, в котором нас интересуют дробные числа. Небрежность состоит в том, что не соблюдается однообразие форматов. Например, имеются и ячейки, отформатированы как текстовые, и ячейки, отформатированные как числа. Ниже показано, как пользователь будет видеть такой файл на машинах с разной локализацией.
Ячейки с числовым форматированием будут отображаться по-разному в зависимости от локализации. Англичанин увидит 136.11, а русский 136,11. Это нормально.
А вот ячейки с текстовым форматированием будут показаны в Excel одинаково при любой локализации. Где-то запятые, где-то точки. Человеку такая каша не сильно помешает, но если такие данные импортировать в Visio, то велика вероятность появления ошибок.
Вид данных в системе с разделителем - точкой.
34 | 98 |
24.4 | 11,46 |
136.11 | 187.4 |
С запятой
34 | 98 |
24.4 | 11,46 |
136,11 | 187,4 |
Естественно, возникает желание создать функцию, приводящую подобные данные к нормальному виду. Она должна работать только с текстовым типом данных, пропуская числовые без изменения. А для преобразования из текстового представления нужно знать действующий в системе десятичный разделитель и заменять точку на запятую либо наоборот. Вот код такой функции.
Private Function ToDbl(ByVal sinp) As Double
If TypeName(sinp) = "String" Then
If IsNumeric("0.1") Then delimiter = "." Else delimiter = ","
If delimiter = "." Then
s = Replace(Trim(sinp), ",", ".")
Else
s = Replace(Trim(sinp), ".", ",")
End If
Else
s = sinp
End If
If IsNumeric(s) Then
ToDbl = CDbl(s)
Else
ToDbl = 0
MsgBox "Ошибка преобразования: " & CStr(sinp)
End If
End Function
И результаты ее работы в обеих системах (справа).
С разделителем - точкой.
34 | 98 | 34 | 98 |
24.4 | 11,46 | 24.4 | 11.46 |
136.11 | 187.4 | 136.11 | 187.4 |
С запятой
34 | 98 | 34 | 98 |
24.4 | 11,46 | 24,4 | 11,46 |
136,11 | 187,4 | 136,11 | 187,4 |
В приведенной функции разделитель вычисляется по выражению IsNumeric("0.1").
If IsNumeric("0.1") Then delimiter = "." Else delimiter = ","
Возможно, более корректно было бы принять значение разделителя непосредственно из системы через Win32API. Однако IsNumeric прельщяет существенно более коротким кодом.
Но на всякий случай приведу и пример использования функции GetLocaleInfo:
Private Declare Function GetLocaleInfo Lib "kernel32" Alias "GetLocaleInfoA" (ByVal LOCALE As Long, _
ByVal LCType As Long, ByVal lpLCData As String, ByVal cchData As Long) As Long
Private Const LOCALE_USER_DEFAULT = &H400
Private Const LOCALE_ILDATE As Long = &H22
Private Const LOCALE_ICOUNTRY As Long = &H5
Private Const LOCALE_SENGCOUNTRY = &H1002 ' English name of country
Private Const LOCALE_SENGLANGUAGE = &H1001 ' English name of language
Private Const LOCALE_SNATIVELANGNAME = &H4 ' native name of language
Private Const LOCALE_SNATIVECTRYNAME = &H8 ' native name of country
Private Const LOCALE_SDECIMAL As Long = &HE
Private Const LOCALE_SLIST As Long = &HC
Private Const LOCALE_STHOUSAND As Long = &HF
Public Function GetInfo(ByVal lInfo As Long) As String
Dim Buffer As String
Dim Ret As String
Buffer = String$(256, 0)
Ret = GetLocaleInfo(LOCALE_USER_DEFAULT, lInfo, Buffer, Len(Buffer))
If Ret > 0 Then
GetInfo = Left$(Buffer, Ret - 1)
Else
GetInfo = ""
End If
End Function
Sub GetInfoExample()
Debug.Print "SList: " & GetInfo(LOCALE_SLIST)
Debug.Print "SDecimal: " & GetInfo(LOCALE_SDECIMAL)
Debug.Print "SThousand: " & GetInfo(LOCALE_STHOUSAND)
Debug.Print "SCountry: " & GetInfo(LOCALE_SENGCOUNTRY)
Debug.Print "SLanguage: " & GetInfo(LOCALE_SENGLANGUAGE)
Debug.Print "VisioListSep(): " & ActivePage.PageSheet.Cells("Scratch.A1").ResultStr(0)
End Sub
Разделитель списков
Сказанное выше касалось в основном десятичного разделителя. Но не менее важен разделитель списков.
Пример из справки - формула =MAX(PageWidth,PageHeight), будучи вписанной в ячейку шейп-листа, отлично работает в любой локали. Это потому что в шейп-листе пользователь видит универсальные формулы, а в универсальной формуле разделитель споска - всегда запятая.
Теперь подлянка от функции SETF. Предположим, мы хотим вписать приведенную выше формулу с помощью SETF, то есть в другой ячейке поместить
=SETF(GetRef(Scratch.D1),"MAX(PageWidth,PageHeight)")
Так вот, это уже будет работать только у американцев, но не будет у итальянцев. Дело в том, что SETF передает формулу в локальную формулу, не универсальную. И в этом случае запятая для итальянцев является криминалом. Чтобы как-то выкрутиться, придумали функцию ListSep(). Пересылающая формула стала такой
=SETF(GetRef(Scratch.D1),"MAX(PageWidth"&LISTSEP()&"PageHeight)")
Вот это работает уже в любой локали, так как в целевую формулу будет подставлена либо запятая, либо точка с запятой.
Другой вариант - если мы пытаемся прописать похожую формулу с помощью VBA. В принципе, можно по аналогии использовать разделитель LOCALE_SLIST, предварительно полученный с помощью GetLocaleInfo. К счастью, это не обязательно. Так как для VBA доступны и Formula, и FormulaU, можно просто использовать FormulaU и всегда писать список через запятую.
Небольшой нюанс - функция ListSep() демонстрирует результат в шейп-листе далеко не сразу после переключения локали. Это не влияет на работу, но может слегка сбить с толку. Если в ячейке записана эта функция, то после изменения разделителя в системе значение ячейки не изменяется. Но после перезаписи формулы все приходит в норму.
Существуют списки, игнорирующие разделитель списков. Например, при использовании в Shape Data фиксированных списков в ячейку Format тоже заносится список. Типа "0.4;0.2;0.3". Разделитель в этом списке на зависит от локали, всегда используется точка с запятой.
Можно еще сказать пару слов по поводу списка в функции RGB. Функция необычна тем, что ее результат заранее непредсказуем. Так как функция действует через таблицу цветов документа, результат вычисляется не напрямую, а с учетом уже имеющихся в таблице значений. Например, при установке .Cells("LineColor").Formula = "RGB(0,0,0)" можем получить следующие значения формул и результата:
RGB(0,0,0) | Formula |
RGB(0,0,0) | FormulaU |
28 | ResultIU |
RGB(0, 0, 0) | ResultStr |
И если значения формул предсказуемы, то вместо значения 28 (ResultIU) вполне может оказаться какое-нибудь другое.
Комбинация RGB с SETF укладывается в рамках вышеизложенного. То есть работает
SETF(GetRef(LineColor),SUBSTITUTE("RGB(0,0,255)",",",LISTSEP())).
Интересные варианты могут получиться при наличии вложенных списков. Например в том же Fixed List в Shape Data может встретиться формула типа
="RGB(0,0,255);RGB(255,0,0);RGB(0,255,0)"
Здесь в одной формуле мы видим список первого уровня с неизменяемым разделителем (точка с запятой) и вложенные списки внутри RGB, которые уже подвержены влиянию локали и при работе через локальную формулу потребуют использования LISTSEP().
Выводы:
Если вы хотите продлить жизнь своим решениям в Visio, желательно делать их нечувствительными к локализации как на уровне макросов, так и на уровне шейп-листа.
1. На уровне макросов старайтесь использовать универсальные формулы .FormulaU вместо локальных .Formula и универсальные разделители.
2. При необходимости записывать макросом дробные значения в локальные формулы используйте разделители, получаемые функцией GetLocaleInfo (или через IsNumeric("0.1")).
3. В шейп-листе совместно с функциями типа SETF следует использовать подстановку разделителя, получаемого функцией LISTSEP().
4. При необходимости импортировать "замусоренные" данные, в которых может присутствовать текстовое представление дробных чисел, следует использовать преобразование типа приведенной выше функции ToDbl.
Приложение
Приложен файл Tester.vsd, послуживший базой для упоминавшихся проверок. В том числе в файле присутствуют пример работы с GetLocaleInfo и функция ToDbl.