пятница, 11 октября 2013 г.

Полезняшки. Переход к FocusControl при клике на TLabel

У TLabel есть такая полезная особенность. Если в Caption добавить символ амперсанда (&), то следующая за этим символом буква становится ускорителем. При этом, при прорисовке заголовка, сам символ скрывается, а буква-ускоритель – подчёркивается. Это означает, что если нажать Alt + <буква>, то произойдёт установка фокуса ввода в контрол, на который ссылается свойство TLabel.FocusControl. Произойдёт автоматически – это нормальное поведение для GUI в Windows.

Однако, если пользователь кликнет мышкой по самому TLabel, то не произойдёт ничего. Я считаю, что это не есть хорошо: интуитивно пользователь ожидает от клика по метке такого же поведения, как и от сочетания клавиш.

Реализовать своими руками необходимое поведение – проще простого. Достаточно в TLabel.OnClick обработчике написать одну строку вида:

Label1.FocusControl.SetFocus;

А теперь представим, что у нас таких меток будет много… Тогда можно создать свой компонент-наследник от стандартного TLabel. Но это не удобно:

  • компонент будет очень простым (почти всего с одной строчкой полезного кода), но его надо выделить в отдельный модуль, да ещё и установить в палитру компонентов;
  • при разработке форм помнить, что надо использовать новый компонент;
  • а если уже есть формы, то надо по ним пройтись и заменить все стандартные TLabel на новый;
  • а если уже используются другие компоненты, расширяющие стандартный TLabel, то придётся и для них делать наследники.

Поэтому мы пойдём другим путём. Привожу сразу код:

type
  TTCustomLabelClick = class
  public
    class procedure DoSelectFocusControl(Sender: TObject);
    class procedure SetLabelClickEvent(ACustomLabel: TCustomLabel); overload;
    class procedure SetLabelClickEvent(AWinControl: TWinControl; CheckChilds: Boolean = True); overload;
  end;

  { TTCustomLabelClick }
  class procedure TTCustomLabelClick.DoSelectFocusControl(Sender: TObject);
  var
    CustomLabel: TCustomLabel;
    AccessLabel: TLabel absolute CustomLabel;
  begin
    CustomLabel := Sender as TCustomLabel;
    if Assigned(AccessLabel.FocusControl) and AccessLabel.FocusControl.CanFocus then
      AccessLabel.FocusControl.SetFocus;
  end;

  class procedure TTCustomLabelClick.SetLabelClickEvent(ACustomLabel: TCustomLabel);
  var
    AccessLabel: TLabel absolute ACustomLabel;
  begin
    AccessLabel.OnClick := TTCustomLabelClick.DoSelectFocusControl;
  end;

  class procedure TTCustomLabelClick.SetLabelClickEvent(AWinControl: TWinControl; CheckChilds: Boolean);
  var
    I: Integer;
    Component: TComponent;
    AccessLabel: TLabel absolute Component;
    WinControl: TWinControl absolute Component;
  begin
    for I := 0 to AWinControl.ComponentCount - 1 do
    begin
      Component := AWinControl.Components[I];
      if (Component is TCustomLabel) and not Assigned(AccessLabel.OnClick) then
        SetLabelClickEvent(AccessLabel)
      else if CheckChilds and (Component is TWinControl) then
        SetLabelClickEvent(WinControl, CheckChilds);
    end;
  end;

Использовать в коде так:

  // Manual set new OnClick behavior:
  TTCustomLabelClick.SetLabelClickEvent(lblUserPassword);

Или так:

  // Automatically set new OnClick behavior for all labels on the form:
  TTCustomLabelClick.SetLabelClickEvent(Self);

Ещё можно второй вариант добавить в базовую форму и забыть об этой проблеме навсегда :)

 

Тестовое приложение:

image

 

Скачать: исходник с тестовым приложением.

13 коммент.:

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

var
CustomLabel: TCustomLabel;
AccessLabel: TLabel absolute CustomLabel;

Бессмысленная и бесполезная хрень.

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

pgolub
Тоже самое я могу написать и про Ваш комментарий. Пожалуйста, аргументируйте и предложите вариант со смыслом.

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

А для чего там вообще переменная CustomLabel? Не понял этого момента.

Torbins комментирует...

Еще один возможный вариант, который правда не работает с наследниками TLabel: http://www.delphikingdom.ru/asp/viewitem.asp?catalogid=1367

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

pgolub, Roman Yankovsky
"Суета" с CustomLabel и AccessLabel заключается в следующем.

а) Свойство FocusControl объявлено на уровне класса TCustomLabel, поэтому важно, чтобы Sender был именно типа TCustomLabel. Для этого используется оператор as. Можно использовать is. А можно и вообще приводить по тихому и свято верить, что никому не вздумается назначить обработчик DoSelectFocusControl на Button, к примеру.

б) Свойство FocusControl на уровне класса TCustomLabel объявлено в protected секции. Поэтому вот так: TCustomLabel(Sender).FocusControl сделать не получится, оно не доступно. А TLabel это свойство публикует в published и мы можем спокойно писать так: TLabel(Sender).FocusControl.

в) Переменная AccessLabel ссылается на тот же объект, что и переменная CustomLabel. В этом фишка absolute. Но при этом, она нужного нам типа. Используется всего лишь с одной целью: упростить читаемость кода. Замените AccessLabel на TLabel(Sender), и вы увидите, на сколько хуже это будет читаться.

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

Torbins
Как вариант - да. Но есть одно но: новый модуль, в котором реализуется новая функциональность, не забыть добавить в uses каждой формы. В ручную это делать довольно тоскливо.

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

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

Вообще, в последнее время, я перестал любить приведения типов вида:
if SomeObject is TSomeClass then
begin
TSomeClass(SomeObject).A;
TSomeClass(SomeObject).B;
end;
Лучше так:
with SomeObject as TSomeClass do
begin
A;
B;
end;
Но с with есть опасность "попасть не туда". Поэтому нагляднее объявить лишнюю переменную нужного типа, а ключевое слово absolute избавит от лишней строки инициализации этой переменной.

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

Николай Зверев, весьма замысловатая идея. Разве что по поводу читаемости не соглашусь. Сейчас это вообще никто не прочитает кроме автора.

Вот так разве не проще?

class procedure TTCustomLabelClick.DoSelectFocusControl(Sender: TObject);
var
AccessLabel: TLabel;
begin
if Sender is TCustomLabel then
begin
AccessLabel := TLabel(Sender);
if Assigned(AccessLabel.FocusControl) and AccessLabel.FocusControl.CanFocus then
AccessLabel.FocusControl.SetFocus;
end;
end;

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

> Вот так разве не проще?
Ну, кому как. Я когда-то "пугался" ключевого слова absolute :). Но в нём нет ничего необычного. Непривычно по началу..

На самом деле, в этом куске кода моя принципиальная позиция только в одном - использовать as а не is. Ибо случайное (или специальное) назначение обработчика не тому контролу - это ошибка. as ошибку покажет, а is позволяет её скрыть.

Можно конечно ещё так сделать: метод DoSelectFocusControl сделать strict private и не делать в нём никаких проверок. А проверка уже есть в SetLabelClickEvent. Наверное у себя я так и сделаю... просто strict private появился в Delphi 2007, а этот код у меня живёт с Delphi 7.

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

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

>> as ошибку покажет, а is позволяет её скрыть.

Вот тут я опять не понял. Неужели код "if Sender is TLabel then" позволит передать в процедуру TButton?

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

Не, если Sender на самом деле будет кнопкой и написать "if Sender is TLabel then", то ошибок не будет, т.к. не отработает условие. И метод ничего не сделает.
Т.е. я считаю ошибкой факт того, что обработчик назначили не тому объекту.
Не ошибкой исполнения, или проектирования, а ошибкой, которую допустил тот программист, который назначил обработчик. А сделать он это мог, например так:
SetLabelClickEvent(TLabel(MyNotLabelObject))

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

В экстремальных условиях тебе приходится работать :)
Но подход с "as" интересен, да, хотя применение absolute мне по-прежнему не очень нравится.

Alex W. Lulin комментирует...

"хрень".. как любят программисты эти слова!!!.. наверное те, что сам "не состоялся"...

По сути - согласен с Янковским. Про absolute - надо стараться забывать. Есть много более правильных техник. Даже то же "приведение в скобках".

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

.

.