Rambler's Top100IT • archiv

rus / eng | Логин | Комментарий к колонке | Печать | Почта | Клуб




Колонки


Новые принципы обработки событий от клавиатуры в JDK 1.3

 
( Вячеслав Фесюнов )

Вступление

Начиная с версии jdk.1.3, введен новый механизм обработки событий от клавиатуры. Он не отменяет существовавшие ранее подобные механизмы. С точки зрения пользователя он является надстройкой над ними. В реализации же, существовавшие ранее механизмы как-бы проецируются на данный вновь введенный механизм обработки событий от клавиатуры и вся работа идет с его помощью. Например, установка горячей клавиши для некоторой кнопки (setMnemonic) приводит к появлению соответствующего элемента во внутренних таблицах, используемых данным механизмом.

К сожалению, толкового описания этих новшеств нет, во всяком случае, мне они не известны. Единственная доступная документация — это Keyboard Bindings in Swing. Но это не руководство по использованию, а, скорее, некоторая внутрифирменная проектная документация. Она содержит всего несколько практических примеров, причем весьма абстрактных и неточных.

Данная статья не претендует на полноту изложения вопроса, а лишь позволяет разобраться в основных элементах нового механизма. В частности, она не затрагивает вопрос, как применять данные средства при разработке собственного Look And Feel.

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

Старые и новые механизмы

Основное преимущество вновь введеных принципов в том, что они являются единообразными и действуют повсеместно. До версии jdk.1.3 для всех компонент, кроме текстовых, регистрацию некоторого действия следовало осуществлять с помощью одного из следующих методов класса JComponent

  void registerKeyboardAction(ActionListener a, String command,
                              KeyStroke k, int condition)
  void registerKeyboardAction(ActionListener a, KeyStroke k,
                              int condition)

Для текстовых компонент (наследники JTextComponent) необходимо было применять другие способы, основанные на построении/модификации объектов класса Keymap.

С введением новых способов можно ограничится изучением одного механизма, который действует как для текстовых, так и для нетекстовых компонент. При этом, те, кто знаком со старыми механизмами могут продолжать с успехом их применять. В Keyboard Bindings in Swing предлагалось объявить методы registerKeyboardAction устаревшими, что и было сделано в версии jdk.1.3. Но уже в jdk.1.3.1 им вернули прежний статус.

Новые средства работы с событиями от клавиатуры базируются на наличии двух отображений (map). Одно из них (класс InputMap) — это отображение множества объектов KeyStroke на некоторое абстрактное множество объектов (обычно, имен действий), которые выступают в качестве ключей во втором отображении - ActionMap. Классы InputMap и ActionMap определены в пакете javax.swing. В InputMap есть методы

 public Object get(KeyStroke keyStroke)

 public void put(KeyStroke keyStroke,
                 Object actionMapKey)

Первый из них позволяет получить ключ к ActionMap, второй — создать новый элемент отображения. В ActionMap есть похожие методы

 public Action get(Object key)

 public void put(Object key,
                 Action action)

Получив значение ключа из InputMap, его используют в качестве параметра метода get для объекта ActionMap, чтобы извлечь действие (Action), соответствующее данной комбинации клавиш (задаваемой KeyStroke). Соответственно, метод put позволяет занести в ActionMap новое отображение ключевого объекта (обычно — строкового имени) на некоторое действие.

Такое двойное отображение реализовано, очевидно, для обеспечения большей независимости. Действительно, можно построить отображение ActionMap, куда занести всевозможные действия с их абстрактными именами. И отдельно построить InputMap, элементы которого ссылаются по именам на те или иные действия, заданные в ActionMap. При этом, на один и тот же элемент ActionMap может быть произвольное количество ссылок из InputMap. Добавление/удаление элементов в/из InputMap может выполняться достаточно независимо от ActionMap. Исключение составляет случай, когда нужно добавить ссылку на действие, отсутствующее в ActionMap. Тогда нужно добавить это действие в ActionMap, дав ему некоторое имя, и добавить элемент в InputMap, задающий соответствие комбинации клавиш (KeyStroke) данному имени.

Небольшое исследование

Для всех основных компонент Swing можно получить их InputMap и ActionMap. Методы getInputMap и getActionMap есть в классе JComponent. Проведем небольшое исследование при помощи следующей программы.

Файл TestKeymaps.java

import javax.swing.*;

public class TestKeymaps {

  public static void main(String[] args) {
    if ( args.length == 0 ) {
        System.out.println(" Вызов: java TestKeymaps <имя_класса>");
        return;
    }
    try {
      Class cls = Class.forName(args[0]);
      JComponent comp = (JComponent)cls.newInstance();
      ActionMap actionMap = comp.getActionMap();
      System.out.println("ActionMap: allKeys =");
      Object akeys[] = actionMap.allKeys();
      for ( int i = 0; i < akeys.length; i++) {
          System.out.println(" "+akeys[i]);
      }
    } catch ( Exception ex ) {
      ex.printStackTrace();
    }
    System.exit(0);
  }
}

Программа предполагает, что в качестве параметра вызова ей передается имя класса - наследника JComponent. Она выдает список имен действий, имеющихся в ActionMap для объекта этого класса.

Вызвав ее для класса JButton

java.exe TestKeymaps javax.swing.JButton

мы получим такой список имен действий:

  pressed
  released

Для JTextField мы получим более длинный список. В нем, в частности, будут такие имена действий, как copy-to-clipboard, paste- from-clipboard, select-word. Эти имена достаточно мнемоничны и по ним можно определить, какие действия им соответствуют.

Тестовая задача

Для знакомства с описываемым механизмом используем принцип постижения его на практике. Поставим некоторую тестовую задачу и будем решать ее в нескольких модификациях.

Многие привыкли использовать такие часто встречающиеся (в редакторах) сочетания клавиш, как Ctrl/INS для копирования в буфер, Shft/INS для вставки из буфера, Alt/BackSpace для отмены предыдущей модификации и др. В Java, в стандартном Look And Feel под Windows используются другие комбинации (Ctrl/C, Ctrl/V для копирования и вставки, соответственно, а отмена вообще не реализована).

Попробуем решить задачу ввода в действие комбинаций клавиш Ctrl/INS и Shft/INS. Можно реализовать и отмену предыдущих модификаций, но для этого следует рассмотреть механизм поддержки Undo/Redo в JFC, а это выходит за рамки данной статьи.

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

Первый вариант решения

Если речь идет об одной конкретной текстовой компоненте в программе, то реализовать это черезвычайно просто. Следующая демонстрационная программа показывает, как это сделать. В ней для первого поля реализовано копирование в буфер при помощи Ctrl/INS, а для второго поля — вставка из буфера по Shft/INS.

Файл TestKeyActions.java

// TestKeyActions.java
// Работа с InputMap и ActionMap

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class TestKeyActions extends JFrame {

  JTextField txt1 = new JTextField(32);
  JTextField txt2 = new JTextField(32);
  JTextArea  txt3 = new JTextArea(3, 32);

  TestKeyActions() {
     super("Программирование действий
      по нажатию на клавиши");

     try  {
       UIManager.setLookAndFeel(
        UIManager.getSystemLookAndFeelClassName());
     }
     catch(Exception e) {
     }

    JComponent c = (JComponent)getContentPane();
    JPanel pn1 = new JPanel();
    BoxLayout b1 = new BoxLayout(pn1, BoxLayout.Y_AXIS);
    pn1.setLayout(b1);
    c.add(pn1, BorderLayout.CENTER);
    JPanel apanel = new JPanel();
    pn1.add(apanel);
    apanel.add(new JLabel("Поле 1"));
    apanel.add(txt1);
    apanel = new JPanel();
    pn1.add(apanel);
    apanel.add(new JLabel("Поле 2"));
    apanel.add(txt2);
    apanel = new JPanel();
    pn1.add(apanel);
    apanel.add(new JLabel("Поле 3"));
    apanel.add(txt3);

//--  Блок модификации InputMap и ActionMap
    txt1.getInputMap().put(
      KeyStroke.getKeyStroke("control INSERT"),
      "copy-to-clipboard");
    txt2.getInputMap().put(
      KeyStroke.getKeyStroke("shift INSERT"),
      "paste-from-clipboard");
//--  Конец блока


    WindowListener wndCloser = new WindowAdapter() {
      public void windowClosing(WindowEvent e) {
        System.exit(0);
      }
    };
    addWindowListener(wndCloser);

    pack();
    setVisible(true);
  }

  public static void main(String[] args) {
    TestKeyActions d = new TestKeyActions();
  }
}

Все действия, относящиеся к рассматриваемому вопросу, сосредоточены в блоке, озаглавленном Блок модификации InputMap и ActionMap. Рассмотрим их подробнее. Для получения объекта KeyStroke, соответствующего комбинации клавиш Ctrl/INS, использован следующий вызов метода getKeyStroke класса KeyStroke:

  KeyStroke.getKeyStroke("control INSERT")

Полученный объект используется в качестве параметра метода put класса InputMap. Вторым параметром является имя действия, которое мы узнали благодаря программе TestKeymaps. Сам объект InputMap получен путем вызова метода getInputMap для первого текстового поля нашего приложения. Это все, что требуется для того, чтобы поставить в соответствие комбинации Ctrl/INS действие copy-to-clipboard, т.е. копирование текста в буфер. Второй оператор этого блока аналогичен первому и назначает действие paste-from- clipboard для комбинации Shft/INS.

Запустив эту программу, можно убедиться, что какой-то результат мы получили. В первом поле можно подсветить набранный текст, скопировать его в буфер по Ctrl/INS и, если после этого перейти во второе поле, то по Shft/INS можно вставить в него данные из буфера. Но это не совсем то, что нам хотелось бы иметь. Указанные назначения клавиш действуют только для отдельных полей визуальной формы. Можно, конечно, сдублировать код и последовательно сделать нужные назначения для всех полей, но понятно, что это не очень хороший выход.

Немного теории

Рассмотрим еще один нюанс, связанный с InputMap и ActionMap. Для каждой компоненты строится один ActionMap и три InputMap. Мы применяли для получения InputMap метод getInputMap без параметра, но есть еще один метод getInputMap — с параметром. В качестве параметра ему может быть передана одна из трех констант, определенных в JComponent: WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, WHEN_FOCUSED или WHEN_IN_FOCUSED_WINDOW. Метод без параметра эквивалентен варианту WHEN_FOCUSED, который используется наиболее часто.

Вариант WHEN_FOCUSED наиболее простой и понятный. Соответствующий InputMap определяет комбинации клавиш, действующие тогда, когда компонента имеет фокус.

Вариант WHEN_ANCESTOR_OF_FOCUSED_COMPONENT означает, что данный InputMap распространяет свое действие на все дочерние компоненты данной компоненты. Его следует использовать тогда, когда нам нужно задать некоторое абстрактное действие, не связанное непосредственно с какой-то компонентой, или действие общее для всех подкомпонент данной компоненты.

И, наконец, вариант WHEN_IN_FOCUSED_WINDOW означает, что данный InputMap действует для всех компонент окна, в котором расположена данная компонента. Он предназначен для определения каких-то действий с компонентой, не зависящих от того, где находится фокус. Примером являются "горячие клавиши".

Обычно, WHEN_ANCESTOR_OF_FOCUSED_COMPONENT используют для компонент-контейнеров, например, для панелей, а WHEN_IN_FOCUSED_WINDOW для элементарных компонент — кнопок, текстовых полей и т.п.

Второй вариант решения

Определим действия копирования/вставки для всех компонент нашего приложения. Для этого выбререм тот контейнер, в котором расположены все наши поля и используем InputMap, соответствующий константе WHEN_ANCESTOR_OF_FOCUSED_COMPONENT.

В качестве такого контейнера больше всего подходит ContentPane окна приложения, получаемый при помощи вызова getContentPane. Результат этого метода имеет тип Container, но в приведенной выше программе он успешно преобразуется к типу JComponent.

По аналогии с предыдущей программой, но с использованием константы WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, перепишем Блок модификации InputMap и ActionMap следующим образом.

//--  Блок модификации InputMap и ActionMap
    InputMap inputMap = c.getInputMap(
JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
    inputMap.put(KeyStroke.
      getKeyStroke("control INSERT"),
      "copy-to-clipboard");
    inputMap.put(KeyStroke.
      getKeyStroke("shift INSERT"),
      "paste-from-clipboard");
//--  Конец блока

Однако, этого недостаточно. Дело в том, что ActionMap для ContentPane не содержит нужных нам действий с именами "copy-to- clipboard" и "paste-from-clipboard". Соответственно, этот вариант работать не будет. Добавить действия в нужный нам ActionMap несложно. Достаточно получить их из ActionMap любого текстового поля и занести в требуемый.

Окончательный вариант выглядит так

//--  Блок модификации InputMap и ActionMap
    InputMap inputMap = c.getInputMap(
JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
    inputMap.put(KeyStroke.
      getKeyStroke("control INSERT"),
      "copy-to-clipboard");
    inputMap.put(KeyStroke.
      getKeyStroke("shift INSERT"),
      "paste-from-clipboard");
    ActionMap actionMap = c.getActionMap();
    Action action = txt1.getActionMap().
      get("copy-to-clipboard");
    actionMap.put("copy-to-clipboard", action);
    action = txt1.getActionMap().
      get("paste-from-clipboard");
    actionMap.put("paste-from-clipboard", action);
//--  Конец блока

Здесь, кроме модификаций InputMap, идет заполнение ActionMap для ContentPane. Сами действия, заносимые в него, выбираются из ActionMap поля txt1.




Справка | Условия Copyright © 1999 — 2008, IT • archiv.
В начало | Логин | Комментарий к колонке | Поиск | Почта