Wiki

Сигнальное Мультиплексирование

Реджинальд Штальбауер

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

Большинство GUI приложений обеспечивают некоторый вид интерфейса множественного документа. Часто это осуществляется с использованием QWorkspace, который обеспечивает структуру MDI, но некоторые приложения используют фиксируемый центральный widget или стек widget со списком документов, в виде значка или с полосой табуляции для переключения между документами. Неважно, как обрабатывается множество документов, только один документ является текущим в определенное время.

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

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

void MainForm::save()
{
  MDIForm *form = (MDIForm*)workspace->activeWindow();
  если (форма)
      form->save();
 }

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

Without a Multiplexer

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

Автоматическая связь и разъединение сигналов может быть достигнута при помощи класса сигнальный мультиплексор. Мы можем использовать сигнальный мультиплексор для того, чтобы:

  • Отделить действия от документов, перенаправляя сигналы действий к текущему документу.
  • Обновить состояния действий, когда текущий документ изменяется.

Класс SignalMultiplexer

Qt уже содержит все необходимые классы и функциональность, чтобы сформировать класс сигнальный мультиплексор. Действия инкапсулируются QAction классом, хотя в некоторых случаях QToolButton или другой widget используется непосредственно. Механизм коммуникации, требуемый для мультиплексирования, обеспечивается сигналами и слотами Qt.

Signal Multiplexer

Определение сигнального мультиплексора выглядит так:

class SignalMultiplexer : public QObject
{
  Q_OBJECT
 
public:
  SignalMultiplexer(QObject *parent = 0, const char *name = 0);
 
  void connect(QObject *sender, const char *signal, const char *slot);
  bool disconnect(QObject *sender, const char *signal, const char *slot);
  void connect(const char *signal, QObject *receiver, const char *slot);
  bool disconnect(const char *signal, QObject *receiver, const char *slot);
 
  QObject *currentObject() const { return object; }
 
public slots:
  void setCurrentObject(QObject *newObject);
 
private:
  struct Connection
  {
      QGuardedPtr<QObject> sender;
      QGuardedPtr<QObject> receiver;
      const char *signal;
      const char *slot;
  };
 
  void connect(const Connection &conn);
  void disconnect(const Connection &conn);
 
  QGuardedPtr<QObject> object;
  QValueList<Connection> connections;
 
};

Самые интересные функции connect () и disconnect (). Вместо вызова QObject:: connect () при создании связи, например при соединении сигнала действия activated () со слотом объекта документа, мы вызываем сначала SignalMultiplexer::connect() функцию и только конкретизируем отправителя, которым является QAction, или QToolButton, и т.п.

Вместо вызова QObject:: connect () для соединения с сигналом объекта документа, например QTextEdit::copyAvailable(bool), к слоту setEnabled(bool) функции приложения "копировать", мы вызываем вторую функцию connect(), конкретизируя действие как получатель.

Соответственно функция disconnect() удаляет связи.

Функция SetCurrentObject(QObject *) должна быть вызвана, когда текущий объект документа изменяется, таким образом, SignalMultiplexer может обновить все связи. Функция CurrentObject() возвращает текущий объект документа.

Реализация класса SignalMultiplexer совершенно проста. Класс запоминает список всех связей в частной переменной connections. Когда setCurrentObject() вызван, все активные связи QObject удаляются, и образовывается новое множество связей к новому текущему объекту документа. Частные функции connect() и disconnect() собственно выполняют работу по установке и удалению QObject связей.

Мы запоминаем все наши ссылки на QObject, используя защищенные (guarded) указатели. QGuardedPtr класс - это класс шаблона, который может быть использован для любого подкласса QObject. QGuardedPtr объект возвращает указатель на QObject и поддерживает операторы -> и * для ссылки. Преимуществом использования QGuardedPtr вместо простого QObject * является то, что QGuardedPtr автоматически устанавливает QObject * . Он возврщает 0, если QObject удален, таким образом гарантируя, что мы не имеем никаких зависших указателей. Он также инициализирует QObject указатель на 0.

Каждый раз, когда вам нужно запомнить указатели QObjects, которыми вы не владеете, хорошо использовать QGuardedPtr чтобы избегать использования недействительного указателя, если QObject удален. QGuardedPtr использует больше памяти, чем обычный QObject *, так что он, возможно, в некоторых случаях не подходит.

Теперь мы готовы посмотреть на реализацию SignalMultiplexer. Мы ограничим себя самым интересным кодом; полный источник доступен в сети.

void SignalMultiplexer::connect(QObject *sender, const char *signal, const char *slot)
{
  Connection conn;
  conn.sender = sender;
  conn.signal = signal;
  conn.slot = slot;
 
  connections << conn;
  connect (conn);
}

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

bool SignalMultiplexer::disconnect(QObject *sender, const char *signal, const char *slot)
{
  QValueList<Connection>::Iterator it =
      connections.begin();
  for (; it != connections.end(); ++it) {
      Connection conn = *it;
      if ((QObject*)conn.sender == sender &&
         qstrcmp(conn.signal, signal) == 0 && qstrcmp(conn.slot, slot) == 0) {
         disconnect(conn);
         connections.remove(it);
         return true;
      }
  }
  return false;
}

Соответственно, функция disconnect() просматривает список связей, пока не найдет связь, соответствующую данному отправителю, сигналу и слоту. Затем она вызывает внутренний disconnect() для удаления связи QObject и выходит из списка.

Другие общие функции connect() и disconnect() действуют похожим образом, исключая использование получателя вместо отправителя:

Теперь посмотрим на внутренние функции connect() и disconnect().

void SignalMultiplexer::connect(const Connection &conn)
{
  if (!object)
      return;
  if (!conn.sender && !conn.receiver)
      return;
 
  if (conn.sender)
      QObject::connect((QObject*)conn.sender, conn.signal, (QObject*)object, conn.slot);
  else
      QObject::connect((QObject*)object, conn.signal, (QObject*)conn.receiver, conn.slot);
}

Код делает некоторую проверку, а затем вызывает QObject::connect () чтобы установить связь. Если отправитель - действительный указатель, текущий объект используется как получатель, однако если получатель уже установлен, текущий объект используется как отправитель.

Соответствующая функция disconnect() почти такая же, только называется QObject:: disconnect ().

Последняя интересная функция - это setCurrentObject(), которая должна быть вызвана, чтобы установить начальный текущий документ, а затем каждый раз, когда текущий объект документа изменяется:

void SignalMultiplexer::setCurrentObject(
      QObject *newObject)
{
  if (newObject == object)
      return;
 
  QValueList<Connection>::ConstIterator it;
  for (it = connections.begin(); it != connections.end(); ++it)
      disconnect(*it);
  object = newObject;
  for (it = connections.begin(); it != connections.end(); ++it)
      connect(*it);
}

Сначала мы должны проверить, действительно ли определенный документ тот же самый, что и текущий объект документа. Если это так, то больше делать ничего не нужно. В противном случае сначала мы просматриваем все связи и вызываем частную функцию disconnect(), так что все текущие QObject связи удаляются. Затем мы присваиваем частный object новому объекту документа и просматриваем все связи вторично, в это время устанавливая QObject связи с использованием нового объекта документа в качестве отправителя или получателя.

Обновление Действий

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

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

Когда изменяется состояние текущего документа, обычно объект документа посылает сигнал с информацией об изменении состояния. Сигнал соединен с соответствующим действием, которое в свою очередь отвечает, изменяя его собственное состояние, чтобы отображать состояние документа. Например, редактор widget может послать сигнал undoAvailable(), если пользователь только что удалил строку текста, и этот сигнал соединился бы с действием "undo", которое в свою очередь сделало бы доступным соответствующую опцию меню и кнопку панели инструментов.

Мы хотим гарантировать, что все действия обновятся каждый раз, когда состояние текущего документа изменится, и каждый раз, когда новый документ становится текущим документом.

Первый шаг - обеспечить функцию для каждого объекта документа, который посылает все сигналы документа с текущим состоянием документа. Мы назовем эту функцию emitAllSignals(). Каждый раз, когда новый текущий документ установлен, мы вызовем эту функцию, что будет приводить к обновлению состояний всех действий.

Нам нужно интегрировать эту функциональность в класс SignalMultiplexer, так что мы определяем следующий интерфейс:

class DocumentObject
{
public:
  virtual &nbsp;DocumentObject() {}
  virtual void emitAllSignals() = 0;
};

Мы должны убедиться, что каждый объект документа также наследует класс DocumentObject и выполняет emitAllSignals().

Мы также должны изменить функцию SignalMultiplexer::setCurrentObject(), добавив следующие строки в конце:

DocumentObject *document = dynamic_cast<DocumentObject*>(newObject);
if (document)
    document->emitAllSignals();

Это означает, что каждый раз, когда setCurrentObject() вызвана, она автоматически обновит состояния всех действий для текущего документа. Это работает при условии, что emitAllSignals() выполняется правильно и все объекты документа наследуют интерфейс DocumentObject.

Мы используем dynamic_cast, чтобы убедиться, что мы только приводим объект к DocumentObject, если он наследует интерфейс DocumentObject Если это не так, dynamic_cast вернет 0, и мы ничего не будем делать с ним. Стиль C вернул бы поврежденный указатель в случае отказа, который мог привести к сбою.

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

Сигнальное мультиплексирование на практике

Итак, мы представили класс SignalMultiplexer и обсудили, как мы можем применить его для сохранения состояний действия.

Посредством примера мы сконструируем самый ограниченный текстовый редактор, который может открывать множество документов, используя интерфейс MDI. Мы будем использовать QWorkspace, чтобы обеспечить MDI, и QMainWindow для нашего главного окна. Каждый документ будет находиться в производном окне QTextEdit MDI. Мы поместим в главное окно QToolBar с действиями "undo" и "redo". Когда пользователь активирует действие, операция "undo" или "redo" должна выполняться на редакторе, активном в настоящий момент, и когда текущий редактор изменяется, состояние действий "undo" и "redo" должно быть доступно или недоступно в зависимости от состояния нового текущего редактора. Кроме того, состояния действия "undo" и "redo" должны также измениться, чтобы отображать взаимодействия пользователя с текущим редактором.

Мы начнем с реализации простого QTextEdit подкласса:

class TextEdit : public QTextEdit, public DocumentObject
{
  Q_OBJECT
 
public:
  TextEdit(QWidget *parent = 0, const char *name = 0);
  void emitAllSignals();
};

Наш подкласс наследует QTextEdit и DocumentObject, чтобы вновь реализовать функцию emitAllSignals(). Реализация выглядит так:

void TextEdit::emitAllSignals()
{
  emit undoAvailable(isUndoAvailable());
  emit redoAvailable(isRedoAvailable());
  ...
}

Единственная задача, которую эта функция выполняет, - это отправка сигналов QTextEdit о текущем состоянии редактора.

Вот - реализация главного окна:

class MainWindow : public QMainWindow
{
  Q_OBJECT
 
public:
  MainWindow(QWidget *parent = 0, const char *name = 0);
  void newDocument();
private slots:
  void currentWindowChanged(QWidget *widget);
private:
  SignalMultiplexer multiplexer;
  QWorkspace *workspace;
};

Интересные элементы здесь - это частный слот currentWindowChanged() и частный член multiplexer. Мы соединим сигнал windowActivated() и слот currentWindowChanged(), и в этом слоте вызовем setCurrentObject(). В идеале мы должны иметь связь windowActivated() прямо со слотом мультиплексора setCurrentObject ( ), но, к сожалению типы параметров не совпадают.

Реализация класса MainWindow выглядит так:

MainWindow::MainWindow(QWidget *parent, const char *name)
  : QMainWindow(parent, name)
{
  QToolBar *tools = new QToolBar("Undo/Redo", this);
  QAction *actUndo = new QAction("Undo", QPixmap("undo.png"), "&Undo", CTRL+Key_Z, this);
  actUndo->addTo(tools);
  ...
  multiplexer.connect(actUndo, SIGNAL(activated()), SLOT(undo()));
  multiplexer.connect(SIGNAL(undoAvailable(bool)), actUndo, SLOT(setEnabled(bool)));
  ...
  workspace = new QWorkspace(this);
  connect(workspace, SIGNAL(windowActivated(QWidget *)),
          this, SLOT(currentWindowChanged(QWidget *)));
  setCentralWidget(workspace);
  newDocument();
  ...
}

Для начала мы создаем панель инструментов и действия. Мы хотим соединить сигнал activated() действия undo и слот TextEdit's undo(). Вместо прямой связи QObject мы заставляем сигнальный мультиплексор установить это соединение вызовом multiplexer.connect(). Проще говоря, мы хотим соединить сигнал TextEdit's undoAvailable(bool) и слот действия setEnabled(bool). Снова мы устанавливаем эту связь, используя мультиплексор.

Затем мы создаем и устанавливаем рабочее пространство. После этого мы вызываем newDocument(), чтобы открыть редактор. В настоящем приложении мы бы имели действие "new document", так что пользователи могут открыть столько редакторов, сколько нужно.

void MainWindow::newDocument()
{
  TextEdit *editor = new TextEdit(workspace);
  editor->setCaption(QString("Editor #%1").arg(workspace->windowList().count() + 1));
  multiplexer.setCurrentObject(editor);
}

Функция создает новый TextEdit и делает его новым текущим документом при помощи вызова функции сигнального мультиплексора setCurrentObject().

Поскольку наш класс TextEdit наследует интерфейс DocumentObject, этот вызов также обновит состояние всех действий, о которых знает мультиплексор. Это срабатывает, так как setCurrentObject() вызывает функцию документа emitAllSignals(). Эмитированные сигналы передают информацию о текущем состоянии редактора действиям, которые соответственно отвечают, например, становясь доступными или недоступными.

Заключение

Эта статья показывает, как обобщить задачу GUI программирования, используя объектную модель Qt с неожиданно небольшим кодом.

Исходный код для класса и для программы-примера предоставлен froglogic.


Copyright© 2003 Trolltech

Trademarks