суббота, 12 декабря 2015 г.

Полезняшки. Переключение раскладки клавиатуры при вводе логина и пароля

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

И есть ещё такая полезняшка: при завершении работы, приложение сохраняет пользовательскую раскладку, а при следующем запуске – восстанавливает её.

Раньше это делалось с помощью пары функций GetKeyboardLayoutName и LoadKeyboardLayout. Первая возвращает строку, а вторая – активирует по строке. Однако в Windows 7 (в отличии от Windows XP) вторая функция сильно и заметно для пользователя тормозит (от 2 до 7 секунд, кто-то пишет до 10).

Не буду долго расписывать поиски решения, там всё (ну почти) тривиально. Просто чтобы не писать код самому, сделал поиск по библиотеке JCL и нашёл модуль JclLocales.

Получилось примерно следующее:

uses
  JclLocales;
..
type
  TAppDataModule = class(TDataModule)
  ..
  strict private
    class var FKeyboradLocalesList: TJclKeyboardLayoutList;
    class var FSavedKL: HKL;
    class constructor Create;
    class destructor Destroy;
  public
    class procedure SaveKLAndSelectEnglish;
    class procedure RestoreKL;
  ..
  end;

class constructor TAppDataModule.Create;
begin
  FKeyboradLocalesList := TJclKeyboardLayoutList.Create;
end;

class destructor TAppDataModule.Destroy;
begin
  FreeAndNil(FKeyboradLocalesList);
end;

class procedure TAppDataModule.SaveKLAndSelectEnglish;
var
  LKbLt: TJclKeyboardLayout;
begin
  Assert(FSavedKL = 0); // для соблюдения парности вызовов, иначе надо делать стек
  FSavedKL := FKeyboradLocalesList.ActiveLayout.Layout;
  LKbLt := FKeyboradLocalesList.LayoutFromLocaleID[$409];
  if Assigned(LKbLt) then
    LKbLt.Activate([klActivate, klSubstituteOK]);
end;

class procedure TAppDataModule.RestoreKL;
begin
  Assert(FSavedKL <> 0);
  FKeyboradLocalesList.ItemFromHKL[FSavedKL].Activate([klActivate, klSubstituteOK]);
  FSavedKL := 0;
end;

И использование где-то в коде:

  TAppDataModule.SaveKLAndSelectEnglish;
  try
    with TfrmChangePassword.Create(AOwner) do
    try
      Mode := NewLgn;
      DB := ADB;
      edLogin.Text := ALogin;
      ActiveControl := edLogin;
      Result := ShowModal = mrOk;
      if Result then
      begin
        ALogin := edLogin.Text;
        APassword := edNewPassword.Text;
      end;
    finally
      Free;
    end;
  finally
    TAppDataModule.RestoreKL;
  end;

Ну примерно так. Спасибо за внимание :)

5 коммент.:

Анонимный комментирует...

Подскажите, с какой целью в описании класса используется ключевое слово CLASS?
И в чем отличие от такого? (если вышеприведенный код написан на Delphi)

type
TAppDataModule = class(TDataModule)
..
strict private
FKeyboradLocalesList: TJclKeyboardLayoutList;
FSavedKL: HKL;
сonstructor Create;
destructor Destroy;
public
procedure SaveKLAndSelectEnglish;
procedure RestoreKL;
..
end;

Николай Зверев комментирует...

Анонимный, в моём примере нет необходимости создавать экземпляр класса.
Т.е. допустимо и безопасно обращаться к методам от имени класса вот так: TAppDataModule.SaveKLAndSelectEnglish.

В Вашем же примере, для доступа к методам SaveKLAndSelectEnglish и RestoreKL необходимо создать экземпляр класса TAppDataModule. Например:
var
..ADM: TAppDataModule;
..ADM := TAppDataModule.Create(..);
..ADM.SaveKLAndSelectEnglish;


Классовые переменные и методы можно расценивать как способ группировки обычных (глобальных) переменных и процедур, а класс-конструктор/деструктор в старых версиях Delphi заменяется на секции initialization и finalization.


Конкретно в примере, приведённом в заметке - если экземпляр TAppDataModule создаётся всегда, то мудрить не обязательно. Просто в моём случае - класс объявлен в одном модуле (и этот модуль доступен всем проектам), а экземпляр создаётся на уровне другого модуля (для каждого проекта - это свой другой модуль) - вот делать зависимость на этот другой модуль я не захотел.

Алексей Тимохин комментирует...

Спасибо! Как раз недавно прикручивал к логин диалогу индикатор нажатого Caps lock-a. А про раскладку и не думал. (впрочем у наших пользователей редко есть русский язык).

А что будет если у юзера нет английской раскладки? Ничего или Exception?

p.s. Кстати, прямо глядя на код возникает риторический вопрос - почему бы не вынести это в один законченный класс TdelphiNotesKbLayoutStateSaver (иначе говоря, и зачем там TDataModule)

Николай Зверев комментирует...

Aleksey Timohin
> если у юзера нет английской раскладки?
Ничего не будет. FKeyboradLocalesList.LayoutFromLocaleID[$409] вернёт nil и ниже есть проверка if Assigned

> почему бы не вынести это в один законченный класс?
Флаг Вам в руки :) У меня в этом необходимости пока нет, для своих проектов я вижу это именно на уровне основного датамодуля.

Алексей Тимохин комментирует...

> Флаг Вам в руки :) У меня в этом необходимости пока нет, для своих проектов я вижу это именно на уровне основного датамодуля.
Спасибо Николай, тут весь код в принципе уже есть в виде отдельного класса. Я из чистого любопытства спросил - всё же Принцип Единственной Ответственности и всё такое.

Отправить комментарий

.

.