1 (изменено: YMP, 2016-06-26 18:52:16)

Тема: АНК: корректная передача русского текста через буфер обмена

Примечание: варианты описанных здесь функций для современных версий 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
}

2

Re: АНК: корректная передача русского текста через буфер обмена

Подправил ClipPutText, чтобы при невозможности открыть буфер память освобождалась. Пока ещё никогда с таким случаем не сталкивался, но мало ли. Получается утечка памяти. В случае успеха, освобождение памяти после записи в буфер обмена не нужно, т.к. она поступает в ведение операционной системы, которая дальше сама о ней позаботится.

3 (изменено: YMP, 2014-12-14 19:58:02)

Re: АНК: корректная передача русского текста через буфер обмена

Присмотримся ко второму фигуранту в деле - переменной ClipboardAll.

Эта переменная предназначена для сохранения всего содержимого буфера обмена в переменную или файл, а впоследствии, при необходимости, восстановления этого содержимого в буфере. Таким образом можно временно использовать буфер для чего-то другого, а потом вернуть к прежнему состоянию.

Вот простой код сохранения и восстановления:

F11::
  Clip:=ClipboardAll ; Читаем всё в переменную.
  Clipboard:=Clip    ; Пишем всё обратно в буфер.
Return

По идее он не должен повлечь за собой никаких сюрпризов - что было, то и стало. Проведём эксперимент. Открываю в Avant Browser страничку с русским текстом, хотя бы эту самую. Переключаю окно на английский, копирую строчку в буфер обмена. Вставляю в Блокноте:

Встроенная переменная Clipboard - удобная вещь

Вставилось нормально, несмотря на англоязычную локаль, поскольку CF_UNICODETEXT, находящийся в буфере, помещён туда самим Avant Browser'ом и закодирован им корректно по таблице 1251. Перекодировка от системы не требуется.

Теперь запускаю скрипт, нажимаю F11, снова вставляю в Блокноте:

Âñòðîåííàÿ ïåðåìåííàÿ Clipboard - óäîáíàÿ âåùü

Что за чертовщина? В буфере явно уже не то, что было, хотя, казалось бы, должно быть всё то же.

Копируем заново и смотрим в буфер обмена. К сожалению, программа InsideClipboard в данном случае не годится, т.к. не показывает форматы в том порядке, как они лежат в буфере, а сортирует их по алфавиту, размеру и т.п. Я использовал самописную функцию. Вот что видим:

DataObject
CF_TEXT
CF_UNICODETEXT
HTML Format
Rich Text Format
Ole Private Data
CF_LOCALE
CF_OEMTEXT

Нажимаем F11 и повторяем просмотр:

DataObject
CF_TEXT
HTML Format
Rich Text Format
Ole Private Data
CF_LOCALE
CF_OEMTEXT
CF_UNICODETEXT

Ну вот, CF_UNICODETEXT уехал почему-то в самый низ, что бы это значило? Я не встречал пока прямых заявлений на этот счёт, но из своих экспериментов сделал вывод, что текстовые форматы, перечисляемые до CF_LOCALE, помещены в буфер обмена приложением, а те, что после, доступны как результат перекодировки системой. Таким образом, CF_UNICODETEXT, закодированный Avant Browser'ом, из буфера исчез и теперь, если будет затребован текст в Юникоде, произойдёт перекодировка с неверной кодовой страницей, в соответствии с локалью.

Дальнейшие опыты показали, что если поместить форматы в другом порядке, чтобы сначала шёл CF_UNICODETEXT, а потом CF_TEXT, то отброшен будет CF_TEXT. Таким образом, AutoHotkey сохраняет в ClipboardAll только самый первый из найденных в буфере обмена форматов простого текста (CF_TEXT, CF_UNICODETEXT, CF_OEMTEXT), а остальные игнорирует. Так что, строго говоря, ClipboardAll - не совсем All. Возможно, это сделано для экономии объёма памяти под переменную и места под файл.

Не в целях саморекламы, но справедливости ради замечу, что ClipGetText() от этого не пострадает, т.к. также работает только с первым найденным форматом, а он-то всегда сохраняется как был изначально.

4

Re: АНК: корректная передача русского текста через буфер обмена

Поправил вызовы функций из msvcrt.dll, обозначив их соглашение вызова (cdecl). Работало и без этого, поскольку AutoHotkey всё равно сам восстанавливает стек после вызова DLL-функции. Но всё-таки в ErrorLevel он сигнализировал о замеченном непорядке.

Также добавил в ClipGetText, в блок после If(Format=0), закрытие буфера обмена — DllCall("CloseClipboard"). Отсутствие этого приводило к сбою — например, при последующей попытке записи в буфер через команду вроде такой:

Clipboard := "Hello"

5 (изменено: YMP, 2016-06-26 18:42:54)

Re: АНК: корректная передача русского текста через буфер обмена

Варианты вышеописанных функций для современных версий AutoHotkey. На юникодных версиях ClipPutText помещает текст в буфер обмена в формате CF_UNICODETEXT, а на ANSI-версии в формате CF_TEXT, т.е. так же, как действует команда Clipboard. Примеры вызова:


ClipPutText(Text, [LocaleID])     ; альтернатива команде Clipboard = %Text%
Text := ClipGetText([CodePage])   ; альтернатива команде Text = %Clipboard%

; Если параметр LocaleID не указан, он считается равным 0x419 (Ru-Ru).
; Если не задан параметр CodePage, он считается равным 1251 (русская ANSI-кодировка).

Сами функции:


ClipPutText(Text, uLocale = 0x419)
{
    static CF_LOCALE := 16, GMEM_MOVEABLE := 2
    TextLen := StrLen(Text)
    If (A_IsUnicode) {
        cbTextBuf := TextLen * 2 + 2
        uFormat := 13   ; CF_UNICODETEXT
    }
    Else {
        cbTextBuf := TextLen + 1
        uFormat := 1    ; CF_TEXT
    }
    hText := DllCall("GlobalAlloc", "uint", GMEM_MOVEABLE, "ptr", cbTextBuf, "ptr")
    hLocale := DllCall("GlobalAlloc", "uint", GMEM_MOVEABLE, "ptr", 4, "ptr")
    If (!hText || !hLocale) {
        ErrorMsg := "Ошибка выделения памяти."
        Goto, Error
    }
    pText := DllCall("GlobalLock",  "ptr", hText, "ptr")
    pLocale := DllCall("GlobalLock", "ptr", hLocale, "ptr")
    StrPut(Text, pText), NumPut(uLocale, pLocale + 0, 0, "uint")
    DllCall("GlobalUnlock", "ptr", hText), DllCall("GlobalUnlock", "ptr", hLocale)
    Opened := DllCall("OpenClipboard", "ptr", 0)
    If (!Opened) {
        ErrorMsg := "Не удалось открыть буфер обмена."
        Goto, Error
    }
    If !DllCall("EmptyClipboard") {
        ErrorMsg := "Не удалось очистить буфер обмена."
        Goto, Error
    }
    If (!DllCall("SetClipboardData", "uint", uFormat, "ptr", hText, "ptr")
    || !DllCall("SetClipboardData", "uint", CF_LOCALE, "ptr", hLocale, "ptr")) {
        ErrorMsg := "Ошибка при записи в буфер обмена."
        Goto, Error 
    }
    DllCall("CloseClipboard")
    Return True
Error:
    If Opened
        DllCall("CloseClipboard")
    If hText
        DllCall("GlobalFree", "ptr", hText)
    If hLocale
        DllCall("GlobalFree", "ptr", hLocale)
    MsgBox,, %A_ThisFunc%, %ErrorMsg%
    Return False
}

ClipGetText(uCodePage = 1251)
{
    static CF_TEXT := 1, CF_UNICODETEXT := 13
    uFormat := 0
    If !DllCall("OpenClipboard", "ptr", 0) {
        MsgBox,, %A_ThisFunc%, Не удалось открыть буфер обмена.
        Return False
    }
    Loop
    {
        uFormat := DllCall("EnumClipboardFormats", "uint", uFormat)
        If (uFormat = 0 || uFormat = CF_UNICODETEXT || uFormat = CF_TEXT)
            Break
    }
    If (uFormat = 0) {
        DllCall("CloseClipboard")
        MsgBox,, %A_ThisFunc%, Нет текста в буфере обмена.
        Return False
    }
    hText := DllCall("GetClipboardData", "uint", uFormat, "ptr")
    pText := DllCall("GlobalLock", "ptr", hText, "ptr")
    If (A_IsUnicode) {
        If (uFormat = CF_UNICODETEXT)
            Text := StrGet(pText)
        Else {
            cbText := DllCall("lstrlenA", "ptr", pText) + 1
            cchText := DllCall("MultiByteToWideChar", "uint", uCodePage, "int", 0
                        , "ptr", pText, "uint", cbText, "ptr", 0, "uint", 0)
            VarSetCapacity(Text, cchText * 2)
            DllCall("MultiByteToWideChar", "uint", uCodePage, "int", 0
                        , "ptr", pText, "uint", cbText, "ptr", &Text, "uint", cchText)
            VarSetCapacity(Text, -1)
        }
    }
    Else {
        If (uFormat = CF_TEXT)
            Text := StrGet(pText)
        Else {
            cchText := DllCall("lstrlenW", "ptr", pText) + 1
            cbText := DllCall("WideCharToMultiByte", "uint", uCodePage, "int", 0
                        , "ptr", pText, "uint", cchText, "ptr", 0, "uint", 0, "ptr", 0, "ptr", 0)
            VarSetCapacity(Text, cbText)
            DllCall("WideCharToMultiByte", "uint", uCodePage, "int", 0
                        , "ptr", pText, "uint", cchText, "ptr", &Text, "uint", cbText, "ptr", 0, "ptr", 0)
            VarSetCapacity(Text, -1)
        }
    }
    DllCall("GlobalUnlock", "ptr", hText)
    DllCall("CloseClipboard")
    Return Text
}