Главная » Delphi » Переключатель с установками

0

На всякий случай перенесем проект LangSwitch из папки Glava7\2 в новую папку (Glava7\3). Для того чтобы ввести механизм передачи параметров, проделаем следующее. Начнем с создания обшей разделяемой области. Для этого добавим к проекту основной программы LangSwitch, как это советует автор [17], отдельный модуль IniHook (File | New | Unit) с секциями initialization и finaiization, определим в нем нужные структуры, перенесем в него в том числе и определение наших сообщений. Здесь мы не будем присваивать им выдуманные из головы значения, а используем процедуру регистрации RegisterWindowMessage с присвоением уникального (в вероятностном смысле) номера. Когда сообщение действует внутри одного оконного класса, делать это необязательно, но у нас оно посылается от DLL конкретному окну, и есть опасность, что кто-то еще пошлет такое же сообщение. Для того чтобы получить уникальный номер, есть два пути. Первый заключается в том, чтобы использовать встроенный генератор случайных чисел Delphi, который вызывается клавишами <CtrI>+<Shift>+<G>, в результате чего генерируется уникальная строка символов, хорошо знакомая по внешнему виду всем, кто занимался поиском по реестру (так еще получают уникальные имена файлов и классов для регистрации в системе). Второй путь— вызов функции createGuiD, которая делает то же самое, но каждый раз заново при запуске приложения. Второй путь несколько более надежен (им часто пользуются авторы вирусов и троянов), но в данном случае его применить нельзя, т. к. нам нужно сообщить полученные номера ловушкам, a DLL компилируется отдельно. Так что мы применим первый способ, и имейте в виду, что сначала нужно компилировать проект программы вместе с модулем IniHook, а уж затем— проект DLL, именно в таком порядке. Сам модуль будет выглядеть так — листинг 7.3.

I Листинг 7.3. Модуль IniHook unit IniHook;

interface urea Windows(Messages;

type

PHooklnfo = ATHookInfo; THooklnfo = packed record

FormHandle: THandle; {дескриптор окна приложения) HookHandleKey: THandle; {дескрипторы ловушек) HookHandleLang: THandle; HookHandleWin: THandle; Key:byte; {скан-код) KeyExt:byte; {расширеннаяиначе 0) Alt’.boolean; {отключать/не отключать) end;

var

DataArea: PHooklnfo = NIL; hMapArea: THandle = 0;

HookMsgKey,HookMsgLangl,HookMsgLang2:integer;

implementation

initialization {создаем файл в памяти:)

hMapArea := CreateFileMapping(SFFFFFFFF, NIL, PAGE_READWRITE, 0,

SizeOf(DataArea), ‘KeyWinLangHook’); DataArea := MapViewOfFile (hMapArea, FII?__MAP_ALL_ACCESS, 0, 0, 0);

{регистрируем свои уникальные сообщения}

HookMsgKey:=RegisterWindowMessage(‘{296EOA43-8114-11D9-BF43- 444553540000}1);

HookMsgLangl:=RegisterWindowMessage(‘{D4EF4AA0-81C6-11D9-BF43- 444553540000}’);

HookMsgLang2:=Regis terWindowMessage(‘{BE5421C0-81C7-11D9-BF43- 444553540000}’);

finalization

{убираем файл из памяти)

if Assigned(DataArea) then UnmapViewOfFile(DataArea);

if hMapArea <> 0 then CloseHandle(hMapArea);

end.

Чтобы сгенерировать три уникальных строки через встроенный генератор Delphi, нужно выждать между нажатиями <Ctrl>+<Shift>+<G> некоторое время (генератор основан на работе системного таймера). Однако в принципе это необязательно, эксперименты показали, что функция RegisterWindowMessage устроена так, что строки могут быть и одинаковыми, а идентификаторы сообщений при этом различаются, и для них сгенерируются все равно разные значения. А вот для сообщения от иконки (ico_Massage) в основной программе проводить подобную операцию совершенно ни к чему, т. к. она зарегистрирована в пределах одного приложения. Ее сообщение никуда дальше не пойдет, и может быть любым в пределах от wm usER до $7FFF.

Поля Key и KeyExt типа Byte в записи, на которую указывает переменная DataArea, будут идентифицировать клавишу, а поле Ait (булевского типа) сигнализировать, нужно ли отключать системную функцию клавиши <Alt>. Ссылка на модуль IniHook автоматически вставится в тексте LangSwitch.pas. А в предложении uses ловушки тоже надо сослаться на этот модуль, секцию же объявления переменных и констант удалить вообще.

Теперь изменим процедуру SetHook в ловушке:

procedure SetHook; stdcall; {установка ловушек} begin

DataArea".HookHandleKey :=

SetWindowsHookEx(WH_KEYBOARD,KeyboardProc, hlnstance, 0); DataArea".HookHandleLang :=

SetWindowsHookEx(WH_GETMESSAGE,LangProc, hlnstance, 0); DataArea".HookHandleWin := SetWindowsHookEx(WH_CBT,WinProc,hlnstance, 0); end;

Во всех функциях надо заменить вызов FindWindow на DataArea". FormHandle, а дескрипторы отдельных ловушек на соответствующие поля в записи, как ранее в процедуре установки (не забывая и процедуру DelHook). Текст, например, функции KeyboardProc теперь будет выглядеть так:

function KeyboardProc(Code: Integer; wParam: wParam; lParam:

lParam) : integer; stdcall; begin (перехват нажатия клавиатуры}

if code<0 then Result:=CallNextHookEx(DataArea".HookHandleKey, code, WParam, LParam)

else begin

Result:=CallNextHookEx(DataArea".HookHandleKey, code, WParam, LParam);

if byte(LParam shr 24)<580 then (только нажатие} begin

if byte(LParam shr 16)= DataArea".Key then (клавиша} if byte(LParam shr 24)= DataArea".KeyExt then {конкретно – дополнительная}

begin

SendMessage (DataArea*. FormHandle, HookMsgKey,

WParam, LParam); Result:=1; end; end;

if (byte (LParam ahr 16)= S38) and (DataArea".Alt=True) then Result:=l; {отключение Alt, как входа в меню} end; end;

Теперь в основной программе LangSwitch.dpr перед установкой ловушки заполним нужные поля в записи:

FillChar(DataArea*, SizeOf(DataArea*), 0); {очищаем разделяемую область)

DataArea*.FormHandle:=FHandle; {передаем дескриптор нашего окна)

DataArea*. Key: –31D; (по умолчанию клавиша Ctrl}

DataArea*.KeyExt:-1; (расширенная – т. е. правая)

DataArea*.А1t:=True; {А1t отключать)

SetHook; {запускаем ловушкиI

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

Кстати, теоретически не мешало бы сделать также еще одну вещь: это мы с вами люди, привычные к работе с клавиатуры, а на свете есть немало пользователей, которые привыкли хвататься за мышь при первой возможности (есть даже чудаки, которые пользуются исключительно экранной клавиатурой». Обратите внимание, что сама Microsoft предусмотрела возможность переключения раскладки мышью, причем в случае, когда языков несколько, выбрать их из меню действительно проще, чем переключаться скрюченными пальцами но кругу. Однако здесь мы не будем этим заниматься — для двух языков это просто нецелесообразно, а упомянутым чудакам наша программа вовсе без надобности. Если же кому-нибудь захочется доделать "переклю- чалку", то укажу возможный путь решения этой проблемы. Основная сложность состоит в том, что при щелчке мышью по иконке окно, для которого раскладка переключается, теряет фокус ввода. Нужно сделать следующее: перехватить в ловушке событие потери фокуса при его переходе конкретно к нашей программе и определить, какое окно его потеряло. Все это в принципе умеет наша ловушка HookHandleWin, если ее немного доработать. Зачем через специальное поле в THookinfo передать дескриптор этого окна в основную программу. Дальше нужно сделать отдельное меню, как и в Microsoft, возникающее по нажатию левой кнопки мыши, и по событию выбора соответствующего пункта устанавливать нужный язык для окна по полученному дескриптору и возвращать тому же окну фокус ввода.

Осталось организовать собственно установку параметров и ее запоминание. Для этого мы добавим к меню еще один пункт Установки (пункт Закрыть должен быть в самом низу):

popMenu:=CreatePopupMenu;

AppendMenu(PopMenu, MFSTRXNG, idmSets, ‘Установки…’); AppendMenu(PopMenu, MF_SEPARATOR, 0," ); AppendMenu(PopMenu, MFSTRING, idmEXIT, ‘Закрыть’);

He забудем в нашу осиротевшую секцию определений констант добавить константу idmSets=2. ‘Гак, а что мы будем делать по пункту Установки? Для того чтобы организовать соответствующий диалог, проще всего добавить к нашему проекту форму и сделать все "по-человечески", средствами Delphi.

Добавим форму (File | New | Form), назовем ее Sets и переместим, как и раньше (см. главу 4), в список Available Forms. В предложение uses основной программы добавим модуль Forms. Кстати, теперь будут доступны и различные поля в пункте Project | Options, так что можно присвоить номер версии, только поле Title заполнять не следует! Установим для формы параметры, как в заставке из главы 4 (Object Inspector у нас изначально отсугствует, поэтому его придется вызвать через пункт View): Borderstyie в bsNone, а Position в poDesktopCenter. Расставим на форме компоненты в соответствии с рис. 7.1, причем сначала нужно установить на форму компонент Panel., а уже на него— все остальные, иначе форма совсем без рамочки получится уж больно некрасивая. Для редактора Editi установим свойство Enabled в False (его мы будем делать доступным при выборе переключателя Другая). По умолчанию, как видите, я установил все параметры в соответствии с выбором правой <Ctrl>, как у нас и есть на самом деле. Двухстрочную надпись Правая (расширенная) пришлось делать двумя компонентами, добавив к CheckBox еще и Label, т. к., в отличие от текста в компонентах Memo, Label и др., заголовки таких компонентов, как CheckBox или RadioRutton, многострочными быть не могут.

Рис. 7.1. Форма для установок переключателя рвсхладки

Теперь опять перейдем к основной программе LangSwitcli.dpr и вставим после создания всплывающего меню строку:

Formi:=ТFormi.Create(Application); /создаем экземпляр формы)

Соорудим обработчик события по выбору пункта Установки из всплывающего меню.

if Msg = wm_COMMAND then begin

if wpr =idmEXIT then CloseAll;

if wpr =idmSets then Formi.Show; (показываем форму) end;

Выход из программы я выделил в отдельную процедуру CloseAll. Теперь надо еще и уничтожать форму, кроме того, у Windows ХР есть "дурная" особенность не выгружать автоматически из памяти неиспользуемые DLL — видимо на всякий случай, вдруг кому пригодится! Чтобы не напрягать никого замусориванием памяти, будем принудительно удалять библиотеку. Так что процедура стала раздуваться, а все время править в двух местах и неудобно и может привести к ошибкам. Выглядеть она будет так:

procedure CloseAll; begin

if Formi <> nil then Formi.Free; (если форма существует – уничтожаем)

Shell_NotifyIcon(NIM_Delete,OnoIconData); (удаляем иконку) DelHook; (удаляем ловушки)

dWin:=GetModuleHandle(‘Langhook.dll’); (дескриптор DLL)

FreeLibrary(dWin); {умаляем DLL) halt; (закрываем программу) end;

Естественно, в обработчике события wm_Destroy тоже надо заменить текст на вызов этой процедуры.

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

Стоит ли показывать форму в модальном режиме? Лично я терпеть не могу модальных диалогов и всячески стареюсь их избегать, кроме случаев, когда от пользователя действительно требуется принятие решения. А если разработчик заставляет пользователя обязательно принимать решение в случае, когда его можно не принимать вовсе — перед вами типичный образчик "программистского мышления". Раз уж вы открыли окно, значит просто обязаны что-то делать, считает такой "программист", а не хотите — заставим. Хороший пример подобного подхода: в прекрасной программе Disco Commander, из всех клонов наиболее близкой к оригиналу в лице Norton Commander, в том числе воспроизведена опция, выдающая предупреждение при попытке закрыть программу. Однако разработчики забыли, что Windows — не DOS, в которой компьютер выключали просто кнопкой питания. И, если вы эту опцию установили, при выходе из Windows у вас зто предупреждение обязательно вылезет. Причем не сразу, естественно, а через некоторое время. В результате, если вы торопитесь, и сразу после нажатия кнопки в меню Завершение работы покинули рабочее место, то, придя обратно, можете обнаружить, что компьютер вовсе и не выключался, а терпеливо ждет, когда вы ему сообщите, что действительно желаете выйти из программы и вообще все выключить. А ведь желание тут было уже высказано пользователем совершенно недвусмысленно — когда он выключал Windows, и задача программиста— обеспечить исполнение этого желания — решена не была. Причем в той же ситуации выводить предупреждение о несохраненном файле — как раз совершенно правильное действие.

Прежде чем перейти к собственно установкам, заранее решим еще одну задачу — их сохранения. Для этого придется создавать дисковый файл (можно сохранять данные и в реестре, но в главе 17 мы окончательно решим этим не заниматься). Восстановим нашу процедуру создания файла из примера про "шпиона" в конце программы, только назовем его, естественно, иначе. В Delphi (точнее, в Windows) есть механизм работы с INI-файлами и со строками типа <параметр>=<значеше> (об обращении с INl-фаичами штатным способом пойдет речь в главе 9), но, честно говоря, я не вижу причин, почему бы здесь нам не организовать все это самим, записав туда просто числа. Мы теряем в наглядности представления, но здесь она не только не требуется, но и вредна, т. к. провоцирует пользователя что-то отредактировать самостоятельно. А зачем ему это может понадобиться, если и сами изменения он будет вносить в установки в лучшем случае пару раз сразу после инсталляции программы? По той же причине мы не будем предельно "вылизывать" сам диалог, хотя и с безразличием тоже отнестись к этому нельзя — работа с диалогом может быть не очень удобной, но по крайней мере не должна создавать проблем.

Установки по умолчанию запишем в файл, если он создается первый раз. а если нет — прочитаем их (установки значений полей записи DataArea, которые делались ранее, кроме дескриптора окна, можно, естественно, удалить).

assiqnfiletft,’LangSwitch.ini’); !файл установок) try

reset(ft); Iпробуем открыть)

readln(ft,DataArea*.Key); {и прочитать)

readln(ft,DataArea*.KeyExt);

readln(ft,st);

if st=1 TRUE’ then DataArea*.Alt:=True else DataArea*.Alt:=False; except {если не получается, то создаем все заново) DataArea*.Key:-?1D; {по умолчанию клавиша Ctrl) DataArea*.KeyExt:=l; {расширенная – т. е. правая) DataArea*.Alt:=True; {Alt отключать) rewrite(ft);

writeln(ft,DataArea*.Key); writeln(ft,DataArea*.KeyExt); writeln(ft,DataArea*.Alt) ; end;

closefile(ft); {закрываем файл)

Заметим, что записывать булевскую переменную в текстовый файл Object Pascal умеет, а вот читать — нет, потому при чтении пришлось "извращаться" со строкой. Далее нужно установить параметры формы-диалога в соответствии с прочитанными значениями:

with Formi do begin

if DataArea*.KeyExt=l then CheckBoxl.Checked:=True else CheckBoxl.Checked:=False;

if DataArea*.Key=S36 then CheckBoxl.Checked:=True;

/для правой клавиши Shift – особое условие) RadioButtonS.Checked:=True; {всегда выбран пункт 5 "Другая") if CheckBoxl.Checked-True then {кроме случаев) case DataArea*.Key of {перечисленных здесь) 538: RadioButtonl.Checked:-True; {Alt) $1D: RadioButton2.Checked:=True; {Ctrl) $36: RadioButton3.Checked:=True; {Shift) $1C: RadioButton4.Checked:=True; {Enter/ end;

Editl.Text:=’S’+hexb(DataArea".Key); and;

Здесь мы использовали модуль Ariphm для вывода текста в ИЕХ-форме, его нужно добавить в uses. Теперь перейдем, наконец, собственно к установкам и создадим в новом модуле процедуру по нажатию клавиши Ок. Выглядеть процедура нажатия клавиши Ok может примерно так:

procedure TForml.ButtonlClick(Sender: TObject); {кнопке Ok) begin

DataArea".Key:=StrToInt(Editl.Text); {обходим правую Shift)

if Editl.Text=’$36* then DataArea*.KeyExt:=0

else

begin

if CheckBoxl.Checked=True then DataArea".KeyExt:=1

{расширенная} else DataArea".KeyExt:=0; {обычная) end;

DataArea".Alt:=CheckBox2.Checked; {Alt, как меню) assignfile(ft,1LangSwitch.ini’); {файл установок! rewrite(ft);

writeln(ft,DataArea*.Key); writeln(ft,DataArea*.KeyExt); writeln(ft,DataArea*.Alt) ; closefile(ft); (закрываем файл)

Forml.Hide; (скрываем формуi end;

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

procedure TForml.RadioButtonSClick(Sender: TObject); begin

Editl.Enabled:=True; end;

procedure TForml.RadioButtonlClick(Sender: TObject); begin Editl.Text:=’$38′;

Editl.Enabled:=False; and;

……  1и т. м.}

Для <Ctrl> (RadioButton2) значение должно быть ‘$10′, для <Shift> (RadioButton3)— ‘$36′ и для <Enter> (RadioButton4)— ‘ $1С’ (см. приложение 2). Обратите внимание, что для правой клавиши <Shift> условие особое, т. к. она отличается от остальных дополнительных клавиш тем, что имеет оригинальный скан-код (см. ранее). Так что для нее устанавливать значение KeyExt в 1 нельзя — не будет работать никакая клавиша. И чтобы не морочить голову "чайникам" такими тонкостями, я принудительно устанавливаю для нее "галку" в пункте Правая (расширенная), и особым образом обхожу это в программе.

Мы обещали не "вылизывать" диалог, и в данном случае это также выразится в том, что мы не будем отслеживать запрещенные комбинации "скан-код + бит расширенной клавиши" — это довольно громоздкая задача, проще будет сделать соответствующее пояснение в справке. Там же надо не забыть отразить момент, связанный с неоднозначностью определения "правый-левый" для расширенных клавиш, которое, как мы помним, относится только к <Alt>, <Ctrl>, <Enter> или <Shift>, но не к случаю дополнительной клавиатуры.

Еще необходимо делать в редакторе проверку на ввод числового значения и на их диапазон (не более 127)— подобные проверки нужно делать всегда. Мы сделаем самым простым способом — объявим переменную oidText:string и будем через нее возвращать старый текст при ошибке во вводе:

procedure TForml.EditlChange(Sender: TObject); begin

if Editl.Texto" then try

if StrToInt(Editl.Text) > 127

then Editl.Text:=01dText; (пробуем преобразовать в число и заодно проверяем диапазон]

except

Editl.Text:=01dText; (если не получается – восстанавливаем} Editl.SelStart:=length(Editl.Text); Editl.SelText:=”; (курсор в конец текста} exit; end;

OidText:=Edit1.Text; end;

procedure TForml.FormShow(Sender: TObject); begin

OldText:=Editl.Text; {запоминаем текстI end;

Способ не слишком удобный для пользователя — автомат есть автомат, попробуйте сами и убедитесь. Использовать же компонент spinEdit, который представляет собой конструкцию Edit + upDown, специально предназначенную для ввода чисел, тут не получится — мы хотим вводить числа также и в шестнадцатеричном виде, a SpinEdit нам этого не позволит. Но наш механизм вполне работает, а "наворачивать" всякие диалоги с объяснением, что именно пользователь сделал неправильно, в данном случае нецелесообразно просто из-за редкости обращения к этому пункту.

Кстати, наша программа имеет один минус — т. к. она работает через скан- коды, то с ее помощью нельзя делать различия между клавишами цифровой клавиатуры при выключенном и включенном <Num Lock> (посмотрите на таблицу в приложении 2). Поэтому использовать удобную возможность, которую дает цифровая "пятерка" при выключенном <Num Lock>, в полной мере не удастся — клавиша также перестанет работать и для ввода цифры "5". Для исправления этой ситуации нужно дополнительно либо отслеживать состояние <Num Lock>, либо анализировать также и виртуальные коды — но такие "навороты" я оставляю читателю в качестве домашнего задания. Неплохо бы также ввести отдельный пункт по выбору режима возвращать/не возвращать обработку клавиши системе — для кого-нибудь опция может оказаться полезной.

Полный текст всех модулей программы вы можете посмотреть на прилагаемом диске. На данном же этапе программа еще не закончена. Это мы сдсласм позднее — добавим справку, пункт О программе, инсталляционный пакет, с помощью которого программу можно будет регистрировать в реестре, и т. п. В частности, при установке целесообразно "убить" системный индикатор раскладки (зачем нам сразу два индикатора?) и выключить пункт Переключать раскладки клавиатуры в Windows ХР — сейчас это приходится делать вручную, а в процессе настоящей инсталляции мы предложим делать это автоматически. Данный проект — хороший пример того, что обеспечение главной функциональности программы (собственно переключения раскладки) очень часто занимает малую часть всего времени, потраченного на ее написание. Сравните наш первый вполне работоспособный проект простейшего переключателя и гораздо более сложный, но и гораздо более "фирменный" окончательный вариант.

 

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

По теме:

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