Тема: АНК: корректная передача русского текста через буфер обмена
Примечание: варианты описанных здесь функций для современных версий AutoHotkey см. в посте №5. В первом посте вариант для AutoHotkey Basic.
Встроенная переменная Clipboard - удобная вещь, сильно упрощающая работу с буфером обмена, но если объектом является русский текст, иногда в результате получаем нечто странное. Слово привет превращается либо в ïðèâåò, либо в ??????.
Чтобы понять, что тут происходит, - немного теории. В буфере обмена могут одновременно находиться разные данные в разных форматах. Скопируем текст в буфер обмена и заглянем туда с помощью, например, программы InsideClipboard (спасибо alexii за ссылку). Получим список наподобие этого:
CF_TEXT
CF_LOCALE
CF_OEMTEXT
CF_UNICODETEXT
Строки 1, 3 и 4 соответствуют скопированному нами тексту в трёх разных форматах, CF_LOCALE - это число, обозначающее локаль ввода, т.е., проще говоря, язык текста. В обычной ситуации двух языков Ru/En это число будет либо 0x409 (En), либо 0x419 (Ru). Текст в буфер обмена помещает не операционная система, а приложение. При этом оно может указать и локаль, если же оно этого не сделает, она будет соответствовать языку окна, из которого взят текст.
Если уж быть совсем точным, локаль ввода - это атрибут не окна, а потока (thread), создавшего окно. Процесс приложения в памяти может состоять из одного или нескольких потоков, каждый поток может создать одно или несколько окон. В последнем случае, переключая язык в одном окне потока, переключим его одновременно и для всех других окон этого потока. (Тут имеются в виду не потоки в терминологии скриптов AutoHotkey, конечно.)
Приложение может поместить текст в буфер обмена в любом формате или сразу в нескольких (AutoHotkey использует CF_TEXT). Другое приложение, куда мы вставляем текст, также может запросить текст из буфера в любом формате. А если такого формата там нет? К примеру, мы скопировали в буфер текст в формате CF_TEXT, что в русской системе соответствует 8-битной кодировке Windows-1251, а приложение, куда нужно вставить, требует текст в Юникоде? В этом случае операционная система осуществляет перекодирование в CF_UNICODETEXT.
И вот здесь решающее значение имеет CF_LOCALE, т.к. именно ею определяется, какая кодовая страница будет использована при перекодировке: русская 1251 или западно-европейская 1252, соответствующая локали 0x409. В последнем случае вместо привет получим ïðèâåò.
Вышеприведённый список форматов текста пусть никого не введёт в заблуждение, это список доступных форматов, а не тех, что реально лежат в буфере. Реально часто лежит один, остальные доступны как результат перекодировки и фактически там появятся только после неё. Так что такая конвертация форматов по запросу - обычное явление.
Рассмотрим обратную ситуацию: текст помещается в буфер обмена в CF_UNICODETEXT (так делает, например, Блокнот), а запрашивается как CF_TEXT (так делает AutoHotkey). Если CF_LOCALE имеет значение 0x409 (окно Блокнота было на английском), для перекодирования используется таблица 1252. Кодам из русского диапазона Юникода там поставлен в соответствие символ ?. Соответственно вместо привет получается ??????.
Итак, корень зла в обоих случаях - западная локаль ввода у потока, помещающего текст в буфер обмена, что вызывает использование не той кодовой страницы при необходимости перекодирования текста.
От этого гораздо реже должны страдать те, у кого в системе язык ввода по умолчанию русский. Тут любой поток при запуске имеет русскую локаль, разве что язык будет намеренно переключен. Но сомневаюсь, что разного рода скриптеры и кодеры поголовно относятся к этой категории. По крайней мере для себя я нашёл русский язык по умолчанию неудобным. В результате любой скрипт AutoHotkey у меня запускается с локалью 0x409, и если я там делаю что-то вроде:
Clipboard=Привет, как дела?
то результат вставки этой строки из буфера в какое-нибудь окно непредсказуем, всё зависит от того, какой формат предпочитает приложение, создавшее это окно.
Для решения проблем с неверной кодовой страницей существует способ правки реестра, который, например, предлагает и Androgen в своём фундаментальном и основополагающем исследовании AutoHotkey и русский язык – дружба навеки. Суть его в том, чтобы параметр 1252 указывал на файл c_1251.nls. Способ действенный, но есть и неудобства. В случае запуска скрипта на другой машине, где реестр не правлен, проблема может вылезти опять. А вы у себя уже успели от неё отвыкнуть и забыли учесть этот момент. Кроме того, страница 1252 иногда бывает полезной - например, для восстановления текста, неправильно перекодированного самим приложением и уже в таком виде помещённого им в буфер обмена. Такое иногда бывает при копировании из PDF.
Короче говоря, тем, кто предпочитает реестр не трогать, могу предложить следующие две функции:
ClipPutText(Text, [LocaleID]) ; альтернатива команде Clipboard=%Text%
Text:=ClipGetText([CodePage]) ; альтернатива команде Text=%Clipboard%
; Если параметр LocaleID не указан, он считается равным 0x419.
; Если не задан параметр CodePage, он считается равным 1251.
ClipPutText помещает в буфер обмена текст в виде CF_TEXT, а также информацию о локали - по умолчанию 0x419. Теперь возможный перевод в Юникод ничем русским буквам не грозит.
ClipGetText просматривает форматы буфера обмена в поисках CF_TEXT или CF_UNICODETEXT и использует тот из них, который встретится первым, исходя из идеи, что первый формат помещён в буфер самим приложением, тогда как второй уже может быть результатом неправильной перекодировки. Если раньше всего найден CF_UNICODETEXT, он конвертируется в CF_TEXT с использованием указанной кодовой таблицы, по умолчанию 1251, таким образом локаль игнорируется.
ClipPutText(Text, LocaleID=0x419)
{
CF_TEXT:=1, CF_LOCALE:=16, GMEM_MOVEABLE:=2
TextLen :=StrLen(Text)
HmemText :=DllCall("GlobalAlloc", "UInt", GMEM_MOVEABLE, "UInt", TextLen+1) ; Запрос перемещаемой
HmemLocale:=DllCall("GlobalAlloc", "UInt", GMEM_MOVEABLE, "UInt", 4) ; памяти, возвращаются хэндлы.
If(!HmemText || !HmemLocale)
Return
PtrText :=DllCall("GlobalLock", "UInt", HmemText) ; Фиксация памяти, хэндлы конвертируются
PtrLocale :=DllCall("GlobalLock", "UInt", HmemLocale) ; в указатели (адреса).
DllCall("msvcrt\memcpy", "UInt", PtrText, "Str", Text, "UInt", TextLen+1, "Cdecl") ; Копирование текста.
NumPut(LocaleID, PtrLocale+0) ; Запись идентификатора локали.
DllCall("GlobalUnlock", "UInt", HmemText) ; Расфиксация памяти.
DllCall("GlobalUnlock", "UInt", HmemLocale)
If not DllCall("OpenClipboard", "UInt", 0) ; Открытие буфера обмена.
{
DllCall("GlobalFree", "UInt", HmemText) ; Освобождение памяти,
DllCall("GlobalFree", "UInt", HmemLocale) ; если открыть не удалось.
Return
}
DllCall("EmptyClipboard") ; Очистка.
DllCall("SetClipboardData", "UInt", CF_TEXT, "UInt", HmemText) ; Помещение данных.
DllCall("SetClipboardData", "UInt", CF_LOCALE, "UInt", HmemLocale)
DllCall("CloseClipboard") ; Закрытие.
}
ClipGetText(CodePage=1251)
{
CF_TEXT:=1, CF_UNICODETEXT:=13, Format:=0
If not DllCall("OpenClipboard", "UInt", 0) ; Открытие буфера обмена.
Return
Loop
{
Format:=DllCall("EnumClipboardFormats", "UInt", Format) ; Перебор форматов.
If(Format=0 || Format=CF_TEXT || Format=CF_UNICODETEXT)
Break
}
If(Format=0) { ; Текста не найдено.
DllCall("CloseClipboard")
Return
}
If(Format=CF_TEXT)
{
HmemText:=DllCall("GetClipboardData", "UInt", CF_TEXT) ; Получение хэндла данных.
PtrText :=DllCall("GlobalLock", "UInt", HmemText) ; Конвертация хэндла в указатель.
TextLen :=DllCall("msvcrt\strlen", "UInt", PtrText, "Cdecl") ; Измерение длины найденного текста.
VarSetCapacity(Text, TextLen+1) ; Переменная под этот текст.
DllCall("msvcrt\memcpy", "Str", Text, "UInt", PtrText, "UInt", TextLen+1, "Cdecl") ; Текст в переменную.
DllCall("GlobalUnlock", "UInt", HmemText) ; Расфиксация памяти.
}
Else If(Format=CF_UNICODETEXT)
{
HmemTextW:=DllCall("GetClipboardData", "UInt", CF_UNICODETEXT)
PtrTextW :=DllCall("GlobalLock", "UInt", HmemTextW)
TextLen :=DllCall("msvcrt\wcslen", "UInt", PtrTextW, "Cdecl")
VarSetCapacity(Text, TextLen+1)
DllCall("WideCharToMultiByte", "UInt", CodePage, "UInt", 0, "UInt", PtrTextW
, "Int", TextLen+1, "Str", Text, "Int", TextLen+1
, "UInt", 0, "Int", 0) ; Конвертация из Unicode в ANSI.
DllCall("GlobalUnlock", "UInt", HmemTextW)
}
DllCall("CloseClipboard") ; Закрытие.
Return Text
}