Оглавление
Структура Конечного автоматаСтруктура конечного автомата предоставляет классы для создания и выполнения графов состояний. Концепция и запись основаны на Диаграммах состояний Harel'а, которые, в свою очередь, основаны на диаграммах состояний UML. Семантика исполнения конечных автоматов основана на State Chart XML (SCXML). Диаграммы состояний предоставляют графический способ моделирования того, как система реагирует на возмущение. Это достигается путем определения возможных состояний системы, и того, как система может переходить из одного состояния в другое (переходы между состояниями). Ключевая характеристика событийно-управляемых систем (таких, как приложения Qt) - это то, что поведение часто зависит не только от последнего или текущего события, но и от событий, предшествовавших этому. Эту информацию легко выразить с помощью диаграммы состояний. Структура конечного автомата предоставляет API и модель исполнения, которая может быть использована, чтобы эффективно встраивать элементы и семантику диаграммы состояний в приложения Qt. Структура тесно связана с существующей мета-объектной системой Qt; например, переходы между состояниями могут быть вызваны сигналами, а состояния могут быть настроены так, чтобы устанавливать свойства и вызывать методы объектов QObject. Система событий Qt используется, чтобы управлять конечными автоматами. Граф состояний в структуре конечного автомата является иерархическим. Состояния могут быть вложены в другие состояния, и текущая конфигурация автомата состоит из множества состояний, которые в данный момент активны. Все состояния в корректной конфигурации автомата будут иметь общего предка. Классы в структуре конечного автоматаЭти классы предоставлены Qt для создания событийно-управляемых конечных автоматов.
Простой конечный автоматЧтобы продемонстрировать функциональность ядра API конечного автомата, просто взгляните на маленький пример: Конечный автомат с тремя состояниями, s1, s2 и s3. Конечный автомат управляется единственной кнопкой QPushButton; когда кнопка нажимается, автомат переходит в другое состояние. В начале, конечный автомат находится в состоянии s1. Диаграмма состояний для этого автомата следующая: Следующий фрагмент показывает код, необходимый для создания такого автомата. Сначала мы создаем конечный автомат и состояния: QStateMachine machine; QState *s1 = new QState(); QState *s2 = new QState(); QState *s3 = new QState(); Затем, мы создаём переходы, используя функцию QState::addTransition(): s1->addTransition(button, SIGNAL(clicked()), s2); s2->addTransition(button, SIGNAL(clicked()), s3); s3->addTransition(button, SIGNAL(clicked()), s1); Далее, мы добавляем состояния в автомат и устанавливаем исходное состояние автомата: machine.addState(s1); machine.addState(s2); machine.addState(s3); machine.setInitialState(s1); В заключение, мы запускаем автомат: machine.start();
Конечный автомат выполняется асинхронно, т.е. он становится частью цикла обработки событий вашего приложения. Выполнение полезной работы по входу и выходу из состоянияВышеупомянутый конечный автомат просто переходит из одного состояния в другое, он не выполняет ни какой работы. Функция QState::assignProperty() может быть использована, чтобы состояние могло устанавливать свойство QObject, когда осуществляется переход в это состояние. В следующем фрагменте, значение, которое должно быть ассоциировано с текстовым свойством QLabel'а, указывается для каждого состояния: s1->assignProperty(label, "text", "In state s1"); s2->assignProperty(label, "text", "In state s2"); s3->assignProperty(label, "text", "In state s3"); Когда осуществляется переход в любое состояние, текст метки будет изменён соответственно. Сигнал QState::entered() посылается, когда автомат входит в состояние, а сигнал QState::exited() посылается, при выходе из него. В следующем фрагменте, слот кнопки showMaximized() будет вызван при входе в состояние s3, а слот кнопки showMinimized() будет вызван при выходе из состояния s3: QObject::connect(s3, SIGNAL(entered()), button, SLOT(showMaximized())); QObject::connect(s3, SIGNAL(exited()), button, SLOT(showMinimized())); При создании своих классов состояний переопределите QAbstractState::onEntry() и QAbstractState::onExit(). Автомат, который завершаетсяКонечный автомат, определённый в предыдущем разделе, никогда не завершится. Для того чтобы конечный автомат мог завершать свою работу, он должен иметь на верхнем уровне конечное состояние (объект QFinalState). Когда конечный автомат входит в состояние окончания работы, он посылает сигнал QStateMachine::finished() и останавливается. Вам нужно установить в конечный автомат объект QFinalState и использовать его в одном или нескольких переходах. Совместное использование переходов посредством группировки состоянийПредположим, мы хотим, чтобы пользователь имел возможность выйти из приложения в любое время, щёлкнув кнопку Quit. Для того чтобы добиться этого, мы нуждаемся в создании конечного состояния и делаем его целью перехода ассоциированного с сигналом clicked() кнопки Quit. Мы можем добавить переход из каждого состояния s1, s2 и s3 в отдельности; однако, это представляется многословным, и нужно было бы также не забыть добавить такой переход от каждого нового состояния, которое будет добавлено в будущем. Мы можем добиться такого поведения (а именно того, что щелчок по кнопке Quit завершает автомат, независимо от того, в каком состоянии он находится), группируя состояния s1, s2 и s3. Это делается, созданием нового состояния верхнего уровня и созданием трёх оригинальных состояний дочерними для нового. Следующая диаграмма показывает новый автомат. Три оригинальных состояния были переименованы в s11, s12 и s13, чтобы отразить то, что они теперь дети нового состояния верхнего уровня, s1. Дочерние состояния неявно наследуют переходы их родительского состояния. Это означает, что теперь достаточно добавить единственный переход из состояния s1 в конечное состояние s2. Новые состояния, добавляемые в s1, также автоматически унаследуют этот переход. Всё что необходимо, чтобы группировать состояния - это указать соответствующего родителя, когда состояние создано. Вам также необходимо указать, какое дочернее состояние является исходным (т.е., в какое дочернее состояние конечного автомата должен происходить переход, когда родительское состояние является целью перехода). QState *s1 = new QState(); QState *s11 = new QState(s1); QState *s12 = new QState(s1); QState *s13 = new QState(s1); s1->setInitialState(s11); machine.addState(s1); QFinalState *s2 = new QFinalState(); s1->addTransition(quitButton, SIGNAL(clicked()), s2); machine.addState(s2); QObject::connect(&machine, SIGNAL(finished()), QApplication::instance(), SLOT(quit())); В этом случае мы хотим выйти из приложения, когда автомат завершится, поэтому сигнал автомата finished() соединён со слотом приложения quit(). Дочернее состояние может перекрывать наследуемый переход. Например, следующий код добавляет переход, который фактически заставляет игнорировать кнопку Quit, когда конечный автомат находится в состоянии s12. s12->addTransition(quitButton, SIGNAL(clicked()), s12); Переход может иметь любое состояние в качестве целевого, то есть целевое состояние не должно быть на том же уровне в иерархии состояний, что и исходное состояние. Использование исторических состояний для сохранения и восстановления текущего состоянияПредставим себе, что мы хотим добавить механизм "прерываний" в пример рассмотренный в предыдущем разделе; пользователь должен иметь возможность нажать кнопку, чтобы конечный автомат мог выполнить некоторую не связанную задачу, после чего конечный автомат должен продолжить делать то, что он делал раньше (т.е. вернутся в прежнее состояние, которое, в этом случае, является одним из s11, s12 и s13). Такое поведение может быть легко смоделировано, используя исторические состояния. Историческое состояние (объект QHistoryState) - это псевдо-состояние, которое представляет дочернее состояние, в котором находилось родительское состояние в последний раз, когда из него вышли. Историческое состояние создаётся как дочернее состоянию, для которого мы хотим записать текущее дочернее состояние; когда автомат обнаруживает присутствие такого состояние во время выполнения, он автоматически записывает текущее (действительное) дочернее состояние, когда происходит выход из родительского состояния. Переход в историческое состояние - это, на самом деле, переход в дочернее состояние, в котором автомат был ранее сохранён; автомат, автоматически "направляет" переход к действительному дочернему состоянию. Следующая диаграмма показывает автомат после добавления механизма прерывания. Следующий код показывает, как он может быть реализован; в этом примере мы просто отображаем сообщение, когда осуществляется переход в состояние s3, затем, немедленно возвращаемся в предыдущее дочернее состояние родительского состояния s1 через историческое состояние. QHistoryState *s1h = new QHistoryState(s1); QState *s3 = new QState(); s3->assignProperty(label, "text", "In s3"); QMessageBox *mbox = new QMessageBox(mainWindow); mbox->addButton(QMessageBox::Ok); mbox->setText("Interrupted!"); mbox->setIcon(QMessageBox::Information); QObject::connect(s3, SIGNAL(entered()), mbox, SLOT(exec())); s3->addTransition(s1h); machine.addState(s3); s1->addTransition(interruptButton, SIGNAL(clicked()), s3); Использование параллельных состояний для предотвращения "комбинаторного взрыва"Предположим, что вы хотите смоделировать набор взаимоисключающих свойств автомобиля в одном конечном автомате. Скажем, свойства, в которых мы заинтересованы - это Clean-Dirty (Чистый-Грязный) и Moving-Not moving (Движущийся-Неподвижный). Это требует четырёх взаимоисключающих состояний и восемь переходов, чтобы можно было представить и свободно перемещаться между всеми возможными комбинациями. Если мы добавим третье свойство (скажем, Red-Blue (Красный-Синий)), то общее количество состояний удвоится, до восьми; и, если мы добавим четвёртое свойство (скажем, Enclosed-Convertible (Закрытый-Кабриолет)), то общее количество состояний удвоится снова, до 16. Используя параллельные состояния, общее количество состояний и переходов растёт линейно по мере добавления свойств, вместо экспоненциального. Более того, состояния могут быть добавлены в параллельное состояние или удалены из него без последствий для любого из его соседних состояний. Чтобы создать группу параллельных состояний, передайте QState::ParallelStates в конструктор QState. QState *s1 = new QState(QState::ParallelStates); // переход в состояния s11 и s12 будет выполнен параллельно QState *s11 = new QState(s1); QState *s12 = new QState(s1); Когда происходит вход в группу параллельных состояний, происходит одновременный вход во все дочерние состояния. Переходы между отдельными дочерними состояниями происходят, как обычно. Тем не менее, любое дочернее состояние может создать переход, который выходит из родительского состояния. Когда это происходит, родительское состояние и все его дочерние завершаются. Параллелизм в каркасе Конечного автомата следует из многоуровневой семантики. Все параллельные операций будут выполнены как один атомарный шаг обработки событий, так что ни при каких обстоятельствах не возможно прервать параллельные операции. Однако события все равно будет обрабатываются последовательно, так как сам автомат однопоточен. К примеру: рассмотрим ситуацию, когда есть два перехода, выходящих из одной группы параллельных состояний, и их условия стали верными одновременно. В этом случае, событие, которое обрабатывается вторым, не будет иметь никакого эффекта, так как первое событие уже вывело автомат из параллельного состояния. Обнаружение того, что составное состояние завершилосьДочернее состояние может быть конечным (объект QFinalState); когда происходит вход в конечное состояние, родительское состояние посылает сигнал QState::finished(). Следующая диаграмма показывает сложное состояние s1, которое делает некоторую обработку перед входом в конечное состояние: Когда происходит вход в конечное для s1 состояние, тогда s1 автоматически посылает сигнал finished(). Мы используем этот сигнал для изменения состояния: s1->addTransition(s1, SIGNAL(finished()), s2); Использование конечных состояний в составных состояниях - удобно, когда вы хотите скрыть внутренние детали составного состояния; т.е. единственное, что внешний мир должен иметь возможность сделать - это войти в состояние, и получить уведомление, когда состояние завершит свою работу. Это очень мощный механизм абстракции и инкапсуляции, когда создаются составные (глубоко вложенные) автоматы. (Конечно, в примере выше вы можете создать переход непосредственно из done состояния s1, вместо сигнала finished() состояния s1, но с последствием, что детали реализации состояния s1 становятся не защищёнными и зависимыми). Для групп параллельных состояний сигнал QState::finished() посылается, когда все дочерние состояния войдут в конечное состояние. Переходы, не имеющие целиПереход может не иметь целевого состояния. Переход без цели может быть вызван так же, как любой другой переход; разница в том, что когда переход без цели срабатывает, это не вызывает никаких изменений состояния. Это позволяет реагировать на сигнал или событие, когда автомат находится в определенном состоянии, не выходя из этого состояния. Пример: QStateMachine machine; QState *s1 = new QState(&machine); QPushButton button; QSignalTransition *trans = new QSignalTransition(&button, SIGNAL(clicked())); s1->addTransition(trans); QMessageBox msgBox; msgBox.setText("The button was clicked; carry on."); QObject::connect(trans, SIGNAL(triggered()), &msgBox, SLOT(exec())); machine.setInitialState(s1); Окно сообщения будет отображаться при каждом нажатии кнопки, а конечный автомат останется в своем текущем состоянии (s1). Если же в качестве цели перехода из s1 указано оно само, то каждый раз будет происходить выход и вход в s1 (т.е. будут посылаться сигналы QAbstractState::entered() и QAbstractState::exited()). События, переходы и защитыQStateMachine запускает собственный цикл событий. Для сигнальных переходов (объектов QSignalTransition), QStateMachine автоматически посылает событие QStateMachine::SignalEvent самому себе, когда он перехватывает соответствующий сигнал; аналогично, для событийных переходов QObject (объектов QEventTransition) посылаются QStateMachine::WrappedEvent. Вы можете послать ваше собственное событие, используя функцию QStateMachine::postEvent(). Когда посылаете пользовательское событие в автомат, вы, как правило, также имеете один или более пользовательских переходов, которые могут произойти из событий этого типа. Чтобы создать такой переход, унаследуйте QAbstractTransition и переопределите QAbstractTransition::eventTest(), чтобы проверить, соответствует ли событие вашему типу (и, возможно, другим критериям, например, свойства объекта событий). Здесь мы определяем наш собственный тип события - StringEvent, для посылки строки в автомат: struct StringEvent : public QEvent { StringEvent(const QString &val) : QEvent(QEvent::Type(QEvent::User+1)), value(val) {} QString value; }; Затем, мы определяем, что переход срабатывает только тогда, когда строка события совпадает с определённой строкой (защищённый (guarded) переход): class StringTransition : public QAbstractTransition { public: StringTransition(const QString &value) : m_value(value) {} protected: virtual bool eventTest(QEvent *e) const { if (e->type() != QEvent::Type(QEvent::User+1)) // StringEvent return false; StringEvent *se = static_cast<StringEvent*>(e); return (m_value == se->value); } virtual void onTransition(QEvent *) {} private: QString m_value; }; В переопределённой функции eventTest(), мы сначала проверяем, является ли тип события желаемым; если это так, то приводим событие к типу StringEvent и выполняем сравнение строк. Следующее - диаграмма состояний, которая использует пользовательское событие и переход: Вот как выглядит реализация диаграммы состояний: QStateMachine machine; QState *s1 = new QState(); QState *s2 = new QState(); QFinalState *done = new QFinalState(); StringTransition *t1 = new StringTransition("Hello"); t1->setTargetState(s2); s1->addTransition(t1); StringTransition *t2 = new StringTransition("world"); t2->setTargetState(done); s2->addTransition(t2); machine.addState(s1); machine.addState(s2); machine.addState(done); machine.setInitialState(s1); Как только автомат будет запущен, мы можем посылать ему события. machine.postEvent(new StringEvent("Hello")); machine.postEvent(new StringEvent("world")); Событие, которое не обрабатывается никаким соответствующим переходом, будет молча проглатываться автоматом. Может оказаться полезным сгруппировать состояния и обеспечить обработку таких событий по умолчанию; например, как показано на следующей диаграмме состояний: Для глубоко вложенных диаграмм состояний, вы можете добавить такой "резервный" переход на том уровне детализации, который наиболее целесообразен. Использование правил восстановления для автоматического восстановления свойствВ некоторых конечных автоматах может быть полезно сосредоточить внимание на назначении свойств в состоянии, а не на восстановлении их, когда состояние больше не является активным. Если Вы знаете, что свойство должно всегда вернуться в свое начальное значение, когда автомат входит в состояние которое явно не дает свойству значения, Вы можете установить глобальное правило возврата QStateMachine:: RestoreProperties. QStateMachine machine; machine.setGlobalRestorePolicy(QStateMachine::RestoreProperties); Когда это правило возврата установлено, автомат автоматически восстановит все свойства. Если он войдет в состояние, где данное свойство не установлено, то он будет сначала искать по иерархии предков, чтобы посмотреть, определено ли свойство там. Если так, то свойству вернется значение, определенной самым близким предком. В противном случае вернется его начальное значение (то есть значение свойства до того как были выполнены какие-либо изменения свойства в состоянии.) Рассмотрим следующий код: QStateMachine machine; machine.setGlobalRestorePolicy(QStateMachine::RestoreProperties); QState *s1 = new QState(); s1->assignProperty(object, "fooBar", 1.0); machine.addState(s1); machine.setInitialState(s1); QState *s2 = new QState(); machine.addState(s2); Он позволяет говорить, что при запуске автомата свойство fooBar = 0.0. Когда автомат будет в состоянии s1, свойство будет равно 1.0, так как состояние явно назначает это значение. Когда автомат находится в состоянии s2, никакое значение явно не определено для свойства, таким образом неявно вернется 0.0. Если мы используем вложенные состояния, то родитель определяет значение для свойства, которое унаследовано всеми потомками, явно не назначающими значение свойства. QStateMachine machine; machine.setGlobalRestorePolicy(QStateMachine::RestoreProperties); QState *s1 = new QState(); s1->assignProperty(object, "fooBar", 1.0); machine.addState(s1); machine.setInitialState(s1); QState *s2 = new QState(s1); s2->assignProperty(object, "fooBar", 2.0); s1->setInitialState(s2); QState *s3 = new QState(s1); Здесь у s1 есть два потомка: s2 и s3. Когда произойдёт вход в s2, у свойства fooBar будет значение 2.0, так как это явно определено для состояния. Когда автомат находится в состоянии s3, никакое значение не определено для состояния, но s1 определяет свойство равным 1.0, таким образом это значение будет назначено fooBar. Анимирование присваивания свойствAPI конечного автомата соединяется с Animation API в Qt, чтобы позволить автоматически анимировать свойства согласно состояниям. У нас есть следующий код: QState *s1 = new QState(); QState *s2 = new QState(); s1->assignProperty(button, "geometry", QRectF(0, 0, 50, 50)); s2->assignProperty(button, "geometry", QRectF(0, 0, 100, 100)); s1->addTransition(button, SIGNAL(clicked()), s2); Здесь мы определяем два состояния пользовательского интерфейса. В s1 кнопка является маленькой, а в s2 она больше. Если мы щелкнем кнопкой для перехода от s1 к s2, то геометрия кнопки будет немедленно установлена. Если мы хотим, чтобы переход был плавным, то всё, что нам нужно, это добавить объект QPropertyAnimation к переходу. QState *s1 = new QState(); QState *s2 = new QState(); s1->assignProperty(button, "geometry", QRectF(0, 0, 50, 50)); s2->assignProperty(button, "geometry", QRectF(0, 0, 100, 100)); QSignalTransition *transition = s1->addTransition(button, SIGNAL(clicked()), s2); transition->addAnimation(new QPropertyAnimation(button, "geometry")); Добавление анимации для рассматриваемого свойства означает, что установка свойства больше не будет вступать в силу немедленно. Вместо этого при входе в состояние начнет играть анимация и плавно анимирует изменение свойства. ак как мы не устанавливаем начальное значение или конечное значение анимации, они будут установлены неявно. Значение начала анимации будет текущим значением свойства, когда анимация начнется, и конечное значение будет установлено основываясь на назначениях свойства, определенных для состояния. Если глобальное правило возвратов конечного автомата установлено в QStateMachine::RestoreProperties, возможно также добавить анимацию для восстановления свойств. Обнаружение того, что все свойства были установлены в состояниеКогда анимации используются чтобы изменить свойства, состояние больше не определяет точные значения, которые будет иметь свойство, когда автомат будет в данном состоянии. В то время пока идёт анимация, у свойства потенциально может быть любое значение, в зависимости от анимации. В некоторых случаях может быть полезно уметь обнаружить, когда свойству фактически назначили значение, определенное состоянием. У нас есть следующий код: QMessageBox *messageBox = new QMessageBox(mainWindow); messageBox->addButton(QMessageBox::Ok); messageBox->setText("Button geometry has been set!"); messageBox->setIcon(QMessageBox::Information); QState *s1 = new QState(); QState *s2 = new QState(); s2->assignProperty(button, "geometry", QRectF(0, 0, 50, 50)); connect(s2, SIGNAL(entered()), messageBox, SLOT(exec())); s1->addTransition(button, SIGNAL(clicked()), s2); Когда нажата кнопка button, автомат перейдет в состояние s2, которое установит геометрию кнопки, и затем выскочит окно сообщения, информирующее пользователя, что геометрия была изменена. IВ нормальном случае, когда анимации не используются, это будет работать как ожидалось. Однако, если для геометрии кнопки будет установлена анимация на переходе между s1 и s2, то анимация начнётся при входе в s2, но свойство geometry фактически не будет достигать своего определенного значения прежде, чем анимация будет закончена. В этом случае, окно сообщения выскочит прежде, чем геометрия кнопки будет фактически установлена. Чтобы гарантировать, что окно сообщения не всплывёт до того, как геометрия не достигнет ее конечного значения, мы можем использовать сигнал состояния propertiesAssigned(). Сигнал propertiesAssigned() будет высылаться когда свойству назначат его конечное значение, сделано ли это немедленно или после того, как анимация закончила играть. QMessageBox *messageBox = new QMessageBox(mainWindow); messageBox->addButton(QMessageBox::Ok); messageBox->setText("Button geometry has been set!"); messageBox->setIcon(QMessageBox::Information); QState *s1 = new QState(); QState *s2 = new QState(); s2->assignProperty(button, "geometry", QRectF(0, 0, 50, 50)); QState *s3 = new QState(); connect(s3, SIGNAL(entered()), messageBox, SLOT(exec())); s1->addTransition(button, SIGNAL(clicked()), s2); s2->addTransition(s2, SIGNAL(propertiesAssigned()), s3); В этом примере, при нажатии кнопки, автомат войдет в s2. Он останется в состоянии s2, пока свойство геометрии не будет установлено в QRect (0, 0, 50, 50). Только потом он перейдет в s3. При входе в s3, выплывет окно сообщения. Если у перехода в s2 будет анимация для свойства геометрии, то автомат останется в s2, пока анимация не закончится. Если не будет такой анимации, то автомат просто установит свойство и немедленно войдет в состояние s3. Так или иначе, когда автомат находится в состоянии s3, Вам гарантируют, что свойству геометрии назначили определенное значение. Если в QStateMachine::RestoreProperties установлены глобальные правила возврата, то состояние также не будет высылать propertiesAssigned (), пока правила не будут выполнены. Что случится, если произойдёт выход из состояния до того, как анимация завершитсяЕсли состояние имеет заданные свойства, и переход в состояние имеет анимацию свойств, потенциально возможен выход из состояния до того как свойствам будут присвоены значения определяющиеся состоянием. Это возможно, когда есть переходы из состояния, которые не зависят от сигнала propertiesAssigned, как описано в предыдущем разделе. API конечного автомата гарантирует, что свойства установленные в конечном автомате:
Когда происходит выход из состояния до завершения анимации, поведение коночного автомата зависит от целевого состояния перехода. Если целевое состояние прямо устанавливает значение свойства, то ни какие дополнительные действия не будут приняты. Свойству будет присвоено значение определённое целевым состоянием. Если целевое состояние не устанавливает какое-либо значение для свойства, есть два варианта: по умолчанию, свойству оставлено значение, которое назначено в состоянии (значение, которое было бы назначено, если бы анимация доиграла до конца). Если установлены глобальные правила возврата, они будут иметь приоритет, и свойства будут восстановлены, как обычно. Анимация по умолчаниюКак описано ранее, Вы можете добавить анимацию к переходам, чтобы удостовериться, что изменения свойств целевых состояний анимируются. Если Вы хотите, чтобы определенная анимация использовалась для данного свойства, независимо от конкретного перехода, Вы можете добавить её как анимацию по умолчанию к конечному автомату. Это в особенности полезно, когда значения свойств, соответствующие состояниям, не известны при разработки автомата. QState *s1 = new QState(); QState *s2 = new QState(); s2->assignProperty(object, "fooBar", 2.0); s1->addTransition(s2); QStateMachine machine; machine.setInitialState(s1); machine.addDefaultAnimation(new QPropertyAnimation(object, "fooBar")); Когда автомат будет в состоянии s2, автомат будет играть анимацию по умолчанию для свойства fooBar, так как этому свойству назначено s2. Заметьте, что анимации, явно установленные на переходах, будут иметь приоритет перд любой анимацией по умолчанию для данного свойства. |
Попытка перевода Qt документации. Если есть желание присоединиться, или если есть замечания или пожелания, то заходите на форум: Перевод Qt документации на русский язык... Люди внесшие вклад в перевод: Команда переводчиков |