Главная » Delphi » Резидентная программа для исправления текста в неправильной раскладке

0

Попробуем на основе нашего прототипа создать программу, которая по нажатию комбинации клавиш <Ctrl>+<F12> исправляет текст, набранный в неправильной раскладке клавиатуры. С целью как можно больше рассказать о методах обращения с функциями API, я покажу процесс поэтапного создания разных вариантов все более работоспособной программы.

Заготовка

Перенесем проект из папки Glava5\2 в другую папку (на диске— Glava5V3), на этот раз через пункт File | Save Project as под новым именем: Layout ("раскладка") . Так как мы не используем модулей, кроме стандартных, то больше ничего в проекте изменять не придется (в том числе и редактировать DSK-файл). Затем удалим из обработчика сообщений ненужные действия (обработка нажатия клавиши <Esc>), а обработку нажатия кнопки мыши перепишем следующим образом:

if Msg = Ico Message then (если это сообщение от иконки} begin

if lpr=WM_RBUTTONUP then {была отпущена ггравая кнопка} begin

if MessageBox(FHandle,’Вы хотите закрыть программу Layout?’,’Warning’,MB_YESNO)=idYES then

begin

Shell_NotifyIcon(NIM_Delete,@no!conData); (удаляем иконку) UnregisterHotKey(FHandle, 1); (убираем горячую клавишу! halt; (эарфываем программу! end; end; end;

Левую кнопку мыши мы освободили— она нам еще понадобится, а при щелчке правой кнопкой на иконке у нас теперь будет возникать соответствующее окно с выбором: закрывать программу или нет. В принципе можно было бы и сократить текст, убрав ненужные пункты при заполнении структуры windowciass (все равно окно показываться не будет), но смысла заниматься этим, рискуя что-то нарушить в программе, нет — структура так или иначе будет занимать место в памяти. Не забудем только заменить текст всплывающей подсказки Projectl на что-то более осмысленное, например Language Layout.

Теперь изменим иконку приложения— раньше мы делали это через меню в свойствах главной формы, но теперь, поскольку ее нет, запустим Image Editor и вручную отредактируем файл ресурса Layout.res, относящийся к нашему проекту. Всю эту операцию надо проделывать при закрытой Delphi, т. е. запускать Image Editor надо не из среды через меню Tools | Image Editor, а через меню Пуск | Программы, папка Borland Delphi 7. Открыв Layout.res в редакторе (пункт File | Open), найдем там иконку (она только одна там и будет), у которой должно быть имя MAINICON, и откроем ее в отдельном окне (для это надо дважды щелкнуть по ее названию). Заранее подберем какую- нибудь подходящую иконку из коллекции, символизирующую, например, редактирование текста (на диске я ее расположил в папке с текущим проектом под названием editl.ico). Откроем файл с этой иконкой в другом окне Image Editor (пункт File | Open с установкой фильтра для файлов ICO — см. рис. 5.1) и заменим одну иконку на другую простым методом Copy-Paste. После этого измененный файл ресурсов надо сохранить (File | Save), закрыть окна с иконками, и закрыть окно Image Editor.

Итак, после того как мы отладили весь ангураж, нам остается оформить саму процедуру исправления раскладки, заменив в ней закрытие программы в обработчике нажатия горячей клавиши. Действия должны быть простыми: выделяем неправильно набранный (например, в Word) текст, нажимаем комбинацию клавиш <Ctrl>+<F12>, после чего текст автоматически заменяется на набранный в противоположной раскладке. Никакой автоматики (определения, действительно ли это бессмысленный текст) вводить здесь не требуется — за исключением того, что раскладка исходного фрагмента должна определиться автоматически. Что. собственно, следует делать программе? Во- первых, забрать выделенный текст в буфер обмена, вывести его, например, в строку, определить раскладку, исправить текст в строке, заменить им то, что в буфере обмена, и снова вставить на место.

Рис. 5.1. Редактор ресурсов

Попытка первая — в лоб

Начнем с первой операции и сначала попробуем пойти самым простым путем. Специально для работы с текстом в буфере есть команды wm_cut, wm_copy и wm paste, которыми можно воспользоваться, например, через функцию SendMessage (для других, не текстовых, форматов в буфере обмена все гораздо сложнее). Но сначала нужно определить, куда именно эти команды посылать — а именно, в то окно, которое в данный момент активно. Для этого есть функция GetForegroundwindow, которая возвращает дескриптор активного окна. Однако у этого окна могут быть дочерние окна, потому воспользуемся дополнительно функцией GetTopwindow, которая возвращает дескриптор дочернего окна (условно назовем его Edit) по известному родительскому. Предварительно в секции описаний надо объявить переменные:

var

WindowHandle, EditHandle: HWnd;

Процедура теперь будет выглядеть так:

if Msg = WM_HOTKEY then (если нажата горячая клавиша)

if wpr=l then (и ее номер 1}

begin

WindowHandle:=GetForegroundWindow; {активное окно} EditHandle:=GetTopWindow(WindowHandle); (активный элемент) SendMessage(EditHandle, WM_COPY, 0, 0);

lПосылаем сообщение для копирования) (пока больше ничего не делаем} end;

Запустите программу и попробуйте — все это будет работать только с Блокнотом, в остальных случаях — полный ноль. Возможно, это связано с тем, что команды эти предназначены для работы в рамках одного приложения, вероятно — с множественным форматом буфера обмена — точно я сказать не могу. Хотя, как видим, можно использовать такой способ для обмена только чисто текстовыми сообщениями.

Вариант второй — посложнее

Остается "нажать мышью на <Reset>" (см. эпиграф), а именно: эмулировать нажатие стандартных клавиш, в данном случае <Ctrl>+<C>. Для этого заменим строку, содержащую вызов SendMessage, на следующий текст:

PostMessage(EditHandle,WM_KEYDOWN,VK_CONTROL,$001D0001); PostMessage(EditHandle,WM_KEYDOWN,ord(‘С’>,$002E0001); SendMessage(EditHandle,WM_COMMAND, $00010043, $00000000);

Функция PostMessage вместо SendMessage используется тогда, когда нам безразлично, что именно произойдет с сообщением — она посылает сообщение без ожидания его обработки, во многих практических случаях они взаимозаменяемы, но не во всех. Дополнительная строка SendMessage с соответствующими параметрами— это так называемый accelerator (ускоритель), иначе PostMessage может не сработать как надо. Параметр wparam, сопровождающий команду wm keydown, содержит виртуальный код клавиши (в данном случае, например, vk Controi). Цифровой параметр (lParam) содержит в двух младших байтах число повторений нажатия клавиши, которое в данном случае равно I, а в третьем байте (шсстнадцатеричные разряды 4 и 5)— скан-код клавиши, т. е. то, что выдает клавиатура (о скан-кодах и виртуальных кодах подробнее см. главу 6). Надо заметить, что, судя по всему, стандартный обработчик сообщения wm keydown скан-код все равно игнорирует, но порядка ради будем делать все, как положено.

Заметки на полях

При необходимости послать сообщение wm_keyup, lparam должен иметь точно такое же значение, но самые старшие два бита должны при этом быть равны 1, т. е., например, для клавиши <С> он будет тогда выглядеть, как $С02Е00С:. И если внимательно вчитаться в то, что написано по поводу этих команд в Win32.hip, а также на сайте MSDN [14]1, то становится ясно, что сама команда wm keyxx вместе с параметром wparam (виртуальный код клавиши, в данном случае ord (‘С’)), и приведенный ранее lparam попросту дублируют друг друга. Хотя старшие два бита в lparam определяют команду (нажата клавиша или отпущена, и ее предыдущее состояние), но скан-код, как и wparam, полностью определяет саму клавишу. Отсюда становится понятно, почему обработчик игнорирует скан-код — но никто не дает гарантии, что какой-то другой обработчик тоже это будет делать!

Запустим программу несколько раз, меняя эмулируемую клавишу с <Ctrl>+<C> на <Ctrl>+<V> (скан-код $2f) и обратно, и убедимся, что копирование и вставка выделенного текста через буфер обмера работает с разными приложениями, включая даже адресную строку браузера— но не со всеми! Например, приведенная процедура не работает с "самым главным редактором", MS Word.

Вариант третий — ура!

Чтобы решить эту проблему, попробуем эмулировать нажатие клавиш по- иному— с помощью функции Keybd event. Выгодное ее отличие от PostMessage заключается в том, что ее использование много проще— не нужно определять никаких дескрипторов, событие посылается "в никуда", т. е. оно будет автоматически перехвачено тем окном, которое находится в данный момент в фокусе. Однако функцию-ускоритель мы все же используем (иначе программа может сбоить), так что определение дескрипторов придется оставить. Кроме того, хотя в числе параметров присутствует скан-код клавиши (второй параметр), в описании функции (см. [14]) разработчики на этот раз честно признались, что он не используется (дальше мы увидим, что они слукавили — это не всегда так). Озаботимся также, чтобы эмулировалось и нажатие клавиш, и их отпускание.

Итак, перенесем проект Layout из папки GlavaS\3 в новую папку (на диске Glava5\4) и заменим наши процедуры в обработчике на следующие:

WindowHandle := GetForegroundWindow; {активное окно} EditHandle := GetTopWindow(WindowHandle); {активный элементI keybd_event(VK_CONTROL,0,0,0); { нажатие клавиши Ctrl} keybd_event (ord (‘0,0,0,0); {нажа тие клавиши С} keybd_event(ord(IC,),0,KEYEVENTF_KEYUP,0); {отпускание С) keybd_event(VK_C0NTR0L,О,KEYEVENTF_KEYUP,0); {отпускание Ctrl } SendMessage(EditHandle,WM_COMMAND, $00010043, $00000000); {ускоритель}

Соответственно, чтобы эмулировать вставку из буфера, надо только заменить ?с’ на ‘V’ в соответствующем месте. Убедимся, что все теперь работает, и пойдем дальше.

Чтобы вывести полученное в строку для дальнейших манипуляций, мы объявим в предложении usee модуль ciipbord, и тогда получим право вписать после приведенной ранее эмуляции нажатия такую строку:

stClb:=Clipboard.AsText; {забрали из буфера в строку}

Разумеется, переменную stcib типа string нам придется заранее объявить. Заодно вместе с ней допишем в секции объявлений две целые переменные- счетчика, которые нам понадобятся чуть позже:

vsr

stClb:string,•

i,n:integer; {счетчики)

Прежде чем двигаться дальше, надо понять, в чем, собственно, состоит процедура перекодировки. Посмотрим на клавиатуру, которая перед вами. Так как никакой системы, связывающей символы в русской и в английской раскладке, нет, то придется соответствие между ними устанавливать табличным способом. Проще всего это сделать так: взять номера символов ASCII подряд и привязать к ним соответствующие по клавиатуре русские символы.

Таблица ASCII, а также коды русских клавиш в различных кодировках приведены в приложении 3 (нам нужна, конечно, Win 1251). Для того чтобы охватить все различающиеся символы (т. е. те, что присутствуют на клавиатуре), достаточно взять фрагмент таблицы ASCII с 34 по 126 символ, но для удобства мы возьмем также и совпадающие в обоих раскладках пробел (символ 32 = $20) и восклицательный знак (33 = $21). Глядя поочередно в таблицу

ASCII, на клавиатуру и затем в таблицу русских кодов, составим таблицу соответствия и занесем ее в виде константы в секции объявлений программы:

const CharEngRus: array (32..126] of byte = ($20, $21, $DD,$B9,$3B,$25, $3F,$FD,$28,$29,$2A, {*} $2B,$E1,S2D,$FE,$2S,$30,$31,$32,$33,$34,$35, 15) $36, $37, $38, S39, $C6, $E6, $C1, $3D, $DE, $2C, $22, <@) $D4, SC8, $D1, $C2, $D3, $C0, $CF, $D0, $D8, $CE, $CB, (K) $C4, $DC, $D2, $D9, $C7, $C9, $CA, 3DB, $C5, $C3, $CC, (V) $D6, $D7, $CD, $DF, $Fb, $5C, $FA, $3A, $5F, $B8, $F4, (a) $E8, $F1, $E2, $F3, $E0, $EF, SFO, $F8, $EE, $EB, $E4, 11) $FC, $F2, $F9, $E7, $E9, $EA, $FB, $E5, $E3, $EC, $F6, (w) $F7,$ED,$FF,$D5,$2F,$DA,$A8 ); var

В конце каждой строки закомментирован знак ASCII, на котором эта строка заканчивается, чтобы было проще контролировать процесс создания. Теперь если мы имеем в английской раскладке символ, скажем, номер 60 (в десятичном представлении, т. е. "<"), то по таблице мы определим, что ему соответствует русский символ с номером $С1, т. е. заглавное "Б".

Заметки на полях

Таблица ASCII устанавливает только первые 128 знакоа, причем первые 32 позиции в этой таблице (от 0 до 31) представляют собой не собственно символы, а управляющие коды (соответствующие в интерпретации для клавиатуры переводу строки, команде <Esc>, <Delete> и т. п.). Поэтому в таблице ASCII, приведенной в приложении 3, вы видите только символы с номерами $20—S7E (32—126), плюс важный для практики код клааиши <Enter> ($0D или 13), минус практически неиспользуемый код с номером 127. Обратите внимание, что виртуальные коды клавиш (см. приложение 2) не всегда совпадают с номерами символов в таблице ASCII. Символы же с номерами 128 и выше зависят от национальной кодовой страницы и от, собстаенно, кодировки, поэтому в том же приложении 3 приведены коды русских букв в различных кодировках. В интересующей нвс здесь кодировке Win1251 русские буквы расположены по порядку в самом конце таблицы (кроме "ё" и "е"), что облегчает задачу определения раскладки. В последней колонке приведены коды русских букв для двухбайтовой кодировки Unicode, к которой мы еще вернемся позже. Напомню, что любой символ, даже тот, для которого не предусмотрено отдельной клавиши, можно ввести с клавиатуры, если "задавить" клавишу <Alt>, а затем набрать номер символа на цифровой клавиатуре при включенной клавише <Num Lock>.

Для того чтобы не сочинять отдельную таблицу для обратного перевода (что гораздо сложнее, т. к. русские буквы и специальные символы расположены в беспорядке), мы воспользуемся следующим приемом: будем просматривать таблицу сначала, пока не наткнемся на номер, соответствующий символу в русской раскладке, и определив, какой ячейке таблицы это соответствует, тем самым узнаем нужный в английской раскладке. Не очень экономичный алгоритм, но при современных скоростях это микросекунды, к тому же Windows тратит гораздо больше времени на куда более пустячные задачи.

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

if Msg = VJM_HOTKEY then {если нажата горячая клавиша)

if wpr=l then {и ее номер 1)

begin

WindowHandle GetForegroundWindow; {активное окно) EditHandle := GetTopWindow(WindowHandle); {активный элемент) keybd_event(VK_CONTROL,0,0,0); { нажатие клавиши Ctrl} keybd__event (ord( ‘С ), 0, 0,0); {нажатие клавиши CI keybd_event(ord(‘С *),0,KEYEVENTF_KEYUP,0); {отпускание С) keybd_event(VK_CONTROL,0,KEYEVENTFJKEYUP, 0); {отпускание Ctrl} SendMessage(EditHandle,WM_COMMAND, $00010043, $00000000);

(ускоритель) stClb:=Clipboard.AsText; (забрали в строку} i:=l; {ишем первый буквенный символ)

while (not (stClb[i] in [‘A’.-‘Z’])) and (not (stClbfi] in [‘а’..’г’])) and (not (stClb[i] in [‘А’..’я’])) do begin i:=i+l; if i>length(stClb) then exit end;

(если ни одного буквенного символа, то на выход) Iопределяем раскладку по найденному буквенному символу:) if (stClb[i] in [‘A’..’Z’]) or (stClb[i] in |’a*..•z’]) then (то это английский} begin

for r.:=l to length (stClb) do

if (ord(stClb[n])>=32) and (crd(stClb[n])<=127) then stClb[nl:=chr(Cha r EngRus[о rd(s tClb[n])]); (заменяем символом из таблицы)

end

else Iэто русский} for n:=l to length(stClb) do for i:=32 to 126 do begin {обратный поиск1 по таблице) if ord(stClb[n] )=CharEr.gRus[i] then

begin stClb[n]:=chr(i); break; end; end;

С1ipboard.Open;

Clipboard.AsText:=stClb; fзабрали обратно в буферI Clipboard.Close; (вставляем поверх выделенного:1

keybd_event(VK_CONTROL,0,0,0); ( нажатие клавиши Ctrl} keybd_event(ord(‘V’),0,0,0); {нажатие клавиши V) keybd_event(ord(‘V1),0,KEYEVENTF_KEYUP,0); {отпускание V) keybd_event(VK_CONTROL,0,KEYEVENTF_KEYUP,0); {отпускание Ctrl ) SendMessage(EditHandle,WM_COMMAND, S00010043, $00000000);

{ускоритель)

end (HocKeyl;

После того как вы воспроизведете все, что здесь написано, или просто попробуете запустить этот вариант программы с диска, вы увидите, что он безупречно работает в среде Windows 98 и MS Word 97, однако в Windows ХР возникают проблемы — во-первых, при работе с множественным буфером обмена в Word 2000 и выше. Во-вторых, что существеннее, в ХР в любом редакторе вместо русского появляются "кракозябры", т. е. компонент, куда происходит вставка из буфера, "не понимает", что перед ним русский. Происходит это потому, что Win9* основана на DOS и однобайтовой кодировке, и переключение второй половины таблицы однобайтовых символов на русскую происходит еще при загрузке системы (и, конечно, только если установлена русская версия, иначе могут быть те же самые проблемы), а ХР— на Unicode, и по умолчанию использует однобайтовую кодовую страницу с поддержкой европейских языков. Причем мы с нашей самодельной программой вправе не считать себя какими-то неполноценными: в точности те же проблемы возникают в ХР при обмене текстами между "официальными" редакторами от MS (Word, WordPad, Notepad) и каким-нибудь редактором, который заведомо Unicode не поддерживает (подробнее об этом см. главу 8). Ко всем этим делам мы еще вернемся позже, когда будем обсуждать Unicode, а сейчас сделаем попытку решить проблему в лоб: попробуем дать понять программе, что перед ней русский. Для этого надо переключить раскладку. Как мы увидим в дальнейшем, переключение кодовой страницы (языка) и переключение раскладки в Windows связаны, поэтому мы, забегая вперед, и попытаемся переключить раскладку перед тем, как интерпретировать содержимое буфера.

Мы еще поговорим подробно в следующих главах о переключателе раскладки, как отдельной программе, а здесь просто попробуем применить эту функцию— если программа определила, что текст был английский и перекодирует его в русский, то она автоматически будет переключать и раскладку на русский и наоборот. Сделать это очень просто с помощью функции LoadKeyboardLayout, которая загружает нужную раскладку по ее названию. Название имеет вид строки, соответствующей 32-битному шестнадцатерич- ному значению, младшие два байта которого есть идентификатор языка в системе Windows, а старшие могут указывать на тип клавиатуры (раскладка Дворака и т. п.), для обычной клавиатуры они равны 0. Список всех идентификаторов языка можно посмотреть, например, в справке по Win32, на сайте [14] или даже просто в реестре, но практически нас интересуют, естественно, только два: русский (0419h) и английский (0409h). Кроме этого, вторым параметром нужно указать флаг, который означает в данном случае просто замену текущей раскладки на свою, что соответствует константе klf activate2. С учетом всего сказанного фрагмент процедуры-обработчика горячей клавиши будет у нас выглядеть следующим образом:

if (stclb [i] in [ ‘A’..’ Z’ ]) or (stClbU] in [‘a’..’z’l)

then {то это английский)

begin

LoadKeyboardLayout(‘00000419′, KLF_ACTIVATE);

{переключаем раскладку на русский} for n:=l to length(stClb) do

if (ord(stClb[n])>=32) and (ord(stClb[n])<=127) then stclb[n]:=chr(CharEngRus[ord(stClb[n])]); {заменяем символом из таблицы)

end

elae {это русскийJ begin

LoadKeyboardLayout(‘00000409′, KLF_ACTIVATE);

{переключаем раскладку на английский) for n:=l to length(stClb) do

Попробуйте, и вы убедитесь, что в Windows 98 все работает нормально — при перекодировке заодно переключается и раскладка. Л вот в Windows ХР опять трудности — во-первых, сама по себе раскладка (если судить по индикатору) не переключается3. Однако текст с английского на русский перекоди

руется нормально, а если установить вручную (клавишами, предназначенными для этого в системе) основную раскладку русскую, то все начинает нормально работать — текст перекодируется туда и обратно (хотя в Word ХР все равно трудности с буфером, но это уже другие проблемы). Разумеется, такой результат нас не устраивает — будем считать попытку неудачной. Оставим пока программу в том виде, в котором она есть, и успокоимся на том, что сделали нормальный вариант для платформы Win9.v. Приступим сначала к программе-переключателю раскладки, а позднее попробуем довести до ума и перекодировщик.

Источник: Ревнч Ю. В.  Нестандартные приемы программирования на Delphi. — СПб.: БХВ-Петербург, 2005. — 560 е.: ил.

По теме:

  • Комментарии