The State Machine Framework

状态机框架提供了用于创建和执行状态图的类。概念和符号是基于Harel的Statecharts: A visual formalism for complex systems,它也是UML状态图的基础。状态机执行的语义是基于State Chart XML (SCXML).的。

状态图提供了图解了系统对于刺激的反应的建模。这是通过定义系统可以处于的状态和系统如何从一个状态转换到另一个状态。事件驱动系统的关键特点是其行为不仅仅依赖于最近或当前的事件,还依赖于先前的事件。通过状态图,这类信息很容易表现出来。

状态机框架提供了API和执行模型,可以用来在Qt程序中有效的嵌入状态图的元素和语义。该框架与Qt的元对象系统紧密的集成在一起。例如,状态的转换可以由信号触发,在QObjects状态可以配置用来设置属性和调用方法,Qt的事件系统是用来驱动状态机的。

状态图在状态机中是分层的。一个状态可以嵌套在另外一个状态里面,状态机的当前配置由一组当前活动的状态组成。状态机中有效配置的所有状态都有一个共同的祖先。

Classesin the State Machine Framework

QKeyEventTransition

QMouseEventTransition

QAbstractState

The base class of states of a QStateMachine

QAbstractTransition

The base class of transitions between QAbstractState objects

QEventTransition

QObject-specific transition for Qt events

QFinalState

Final state

QHistoryState

Means of returning to a previously active substate

QSignalTransition

Transition based on a Qt signal

QState

General-purpose state for QStateMachine

QStateMachine

Hierarchical finite state machine

QStateMachine::SignalEvent

Represents a Qt signal event

QStateMachine::WrappedEvent

Inherits QEvent and holds a clone of an event associated with a QObject

ASimple State Machine

为了演示状态机API的核心功能,我们来看一个简单的例子:一个状态机有三个状态S1,S2和S3。状态机由一个QpushButton控制:当点击button状态机转换到另一个状态。起初,状态机处于状态S1,状态机的状态图如下:

以下显示了创建这样的一个状态机所需的代码。首先,我们创建状态机和状态:

QStateMachine machine;
    QState*s1 =newQState();
    QState*s2 =newQState();
    QState*s3 =newQState();

然后,我们用 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();

状态机异步执行,成为程序事件循环的一部分。

DoingUseful Work on State Entry and Exit

以上状态机只不过是进行了状态转换,没有完成任何操作。当状态机进入某一个状态时,可以用QState::assignProperty() 函数来设置QObject的属性。以下代码段,QLabel'的text属性的值根据了每个状态赋值。

s1->assignProperty(label,"text","In state s1");
    s2->assignProperty(label,"text","In state s2");
    s3->assignProperty(label,"text","In state s3");

当进入任意状态,label的text值就会相应的改变。

当进入某一个状态就会发出QState::entered()信号,退出某个状态时会发出QState::exited() 信号。以下代码中,当进入状态S3button的showMaximized() 就会被调用,退出状态S3时button的showMinimized()被调用。

  QObject::connect(s3, SIGNAL(entered()), button, SLOT(showMaximized()));
    QObject::connect(s3, SIGNAL(exited()), button, SLOT(showMinimized()));

自定义状态可以重新实现 QAbstractState::onEntry() 和 QAbstractState::onExit()。

StateMachines That Finish

在前面章节定义的状态机不会结束,为了让状态机能终止,它需要有一个顶级的结束状态。当状态机进入顶级终止状态,状态机将发出QStateMachine::finished() 信号并终止。

为状态图引入终止状态,你所需要做的就是创建一个QFinalState 对象,并把它作为一个或多个转换的目标。

SharingTransitions By Grouping States

假定我们想让用户在任意时候点击Quit button都能退出程序,为了实现这样的功能,我们需要创建一个终止状态并把它作为连接到 Quit button clicked() 信号的转换的目标。我们可以为状态S1,S2和S3都添加转换;然而,这看起来是多余的,而且我们必须记得为将来可能添加的新状态添加一个转换。

要达到同样的效果,我们可以通过为状态S1,S2和S3进行分组。我们可以创建新的顶级的状态并把原来的三个状态作为新状态的子状态。下图显示了新的状态机:

原来的三个状态被重命名为S11,S12和S13以体现它们是新的顶级状态S1的子状态。子状态隐式继承了父状态的转换,这意味着只需要添加一个从状态S1到终止状态S2的转换就足够了。新加入到S1的状态也自动的继承该转换。

状态分组所需要的就是在创建状态时要指定适当的父状态。你也需要指定哪个子状态是起始状态。

QState*s1 =newQState();
    QState*s11 =newQState(s1);
    QState*s12 =newQState(s1);
    QState*s13 =newQState(s1);
    s1->setInitialState(s11);
    machine.addState(s1);
    QFinalState*s2 =newQFinalState();
    s1->addTransition(quitButton, SIGNAL(clicked()), s2);
    machine.addState(s2);
    machine.setInitialState(s1);
 
    QObject::connect(&machine, SIGNAL(finished()),QApplication::instance(), SLOT(quit()));

在这种情况下,我们想程序在状态机终止时退出,所以状态机的finished() 信号被连接到程序的 quit() 槽函数。

子状态可以重写继承的转换。例如,下面代码添加了一个转换使得当状态机处于S12时Quit button被忽略。

s12->addTransition(quitButton, SIGNAL(clicked()), s12);

任何状态都可以作为转换的目标。目标状态不必与源状态在同一个状态等级。

UsingHistory States to Save and Restore the Current State

想象一下,我们想在上面章节讨论的例子中添加一个中断机制;用户应该可以点击一个button让状态机完成不相关的任务,然后状态机应该可以之前所做的操作。

这样的行为可以用history states来建模,历史状态是体现了父状态上次退出的状态的伪状态。

一个历史状态创建作为一个子状态,其父状态就是我们想记录当前子状态的一个状态。当状态机在运行时发现这样的一个状态,当父状态退出时它自动的记录当前子状态。到历史状态的转换实际上是到状态机之前保存的子状态的转换。状态机自动向前转换到真正的子状态。

下图显示了添加中断机制后的状态机:

以下代码显示了它是如何实现的:在这个例子中,当进入了状态S3,我们只是简单的显示一个 message box ,然后通过历史状态立即返回到S1的先前的子状态。

    QHistoryState*s1h =newQHistoryState(s1);
 
    QState*s3 =newQState();
    s3->assignProperty(label,"text","In s3");
    QMessageBox*mbox =newQMessageBox(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);

UsingParallel States to Avoid a Combinatorial Explosion of States

假定你想在一个状态机中建模一个汽车的一组相互排斥的属性。比如说属性 Clean 和 Dirty,Moving 和 Not moving。那就需要4中相互排斥的状态和8个转换来表达它们之前的所有可能的自由移动。

如果我们添加第三个的属性,状态的数量将翻倍,达到8个。如果我们第四个属性,状态的数量再翻倍,达到16个。

使用平行状态,状态的数量和转换个数随着我们添加新的属性线性增加,而不是指数增长。此外,添加和移除状态并不影响兄弟状态。

要创建平行状态组, 需要为 QState的 构造函数传递QState::ParallelStates 参数。

QState*s1 =newQState(QState::ParallelStates);
    // s11 and s12 will be entered in parallel
    QState*s11 =newQState(s1);
    QState*s12 =newQState(s1);

当进入一个平行状态组,会同时进入其所有的子状态,单独子状态之间的转换正常运行。然而,任意一个子状态可能执行一个转换退出父状态。这时,父状态和其所有的子状态将退出。

在状态机框架的并行性遵循了一个交错的语意。所有的平行操作将执行在一个单原子的事件处理步骤中,所以没有任何事件可以中断平行操作。然而,事件仍然按顺序处理,因为状态机是单一线程的。例如,考虑这样一种情况,有两个退出同一个平行状态组的转换,它们的条件同时为true。在这种情况下,在这两个转换之后处理的事件将不起任何作用,因为第一个事件已经使得状态机从平行状态中退出。

Detectingthat a Composite State has Finished

子状态可以是终止状态:当进入一个终止状态,其父状态发出QState::finished()信号。下图显示了组合状态S1在进入终止状态前进行了一些处理:

当S1进入终止状态,S1自动的发出 finished()信号。我们用单一的转换引起该事件去触发一个状态改变:

s1->addTransition(s1, SIGNAL(finished()), s2);

当你想隐藏组合状态内部细节时,在组合状态中使用终止状态是很有用的。外界能做的就是进入该状态和当状态完成工作时得到通知。当创建复杂的状态机时这是一个很强大的抽象和封装机制。

对于平行状态组,当所以的子状态都进入结束状态才会发出 QState::finished() 信号。

TargetlessTransitions

一个转行不必有目标状态。没有目标状态的转换可以像其他转换一样被触发,不同的是:当无目标状态的转换被触发,不引起任何状态变化。这允许当你的状态机处于特定的状态对某个信号或事件作出反应,却不用离开其状态。例如:

QStateMachine machine;
QState*s1 =newQState(&machine);
 
QPushButton button;
QSignalTransition*trans =newQSignalTransition(&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);

每次点击 button的时候messagebox 都会显示,但是状态机还是处于当前的状态S1。如果目标状态被显示的设置为S1,每次都会退出S1并重新进入S1状态。

Events,Transitions and Guards

一个QStateMachine 运行自己的事件循环。对于信号转换,当它拦截到相应的信号,QStateMachine 自动发送QStateMachine::SignalEvent 到它本身。同样的,对于QObject事件转换则发出QStateMachine::WrappedEvent

你可以用QStateMachine::postEvent().给状态机发送自己的事件。

当给状态机发送一个自定义的事件,你通常有一个或多个自定义的转换可以由该类事件触发的转换。要创建这样的转换,你需要继承 QAbstractTransition 并重新实现QAbstractTransition::eventTest(),,在函数中检查一个事件是否与定义的类型匹配。

这里我们定义了一个事件类型,StringEvent,用来传递string给状态机:

struct StringEvent : publicQEvent
{
    StringEvent(constQString&val)
    : QEvent(QEvent::Type(QEvent::User+1)),
      value(val) {}
 
    QString value;
};

接着,我们定义一个转换,仅仅当事件的字符串匹配特定的字符串才会被触发:

class StringTransition : publicQAbstractTransition
{
    Q_OBJECT
 
public:
    StringTransition(constQString&value)
        : m_value(value) {}
 
protected:
    virtual bool eventTest(QEvent*e)
    {
        if (e->type() !=QEvent::Type(QEvent::User+1)) // StringEvent
            returnfalse;
        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 =newQState();
    QState*s2 =newQState();
    QFinalState*done =newQFinalState();
 
    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"));

没有被任何相关的转换处理的事件会被状态机默默的消耗掉,状态分组并为这类事件提供默认处理是很有用的。如以下状态图所示:

为了深度嵌套状态图,你可以在最适合的粒度级别添加这样回退的转换。

UsingRestore Policy To Automatically Restore Properties

在一些状态机中,专注于在状态中设置属性是很有用的,不是当状态不在活动时恢复它们。如果你知道当状态机进入一个不明确给属性一个值的状态时,一个属性应该总是被恢复到起始值, 你可以设置全区恢复策略到QStateMachine::RestoreProperties.。

QStateMachine machine;
machine.setGlobalRestorePolicy(QStateMachine::RestoreProperties);

当设置了恢复策略,状态机将自动的恢复所有的属性。如果状态机进入一个给定的属性没有设置值的状态中,它会搜索祖先层级,看属性是否被定义。如果是,属性将被恢复到最近祖先定义的值。如果不是,它将被恢复到起始值。

QStateMachine machine;
    machine.setGlobalRestorePolicy(QStateMachine::RestoreProperties);
 
    QState*s1 =newQState();
    s1->assignProperty(object,"fooBar",1.0);
    machine.addState(s1);
    machine.setInitialState(s1);
 
    QState*s2 =newQState();
    machine.addState(s2);

AnimatingProperty Assignments

在Qt中,状态机API连接动画API,允许在状态中指定自动动画属性。

  QState*s1 =newQState();
    QState*s2 =newQState();
 
    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状态button是小的,在S2状态button比较大。如果我们点击button从S1转换到S2,当进入某个状态之后button的几何大小就立即被设置了。如果我们想让转换平滑一些,我们所需要做的就是创建一个 QPropertyAnimation 对象并添加到转换中。

QState*s1 =newQState();
    QState*s2 =newQState();
 
    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(newQPropertyAnimation(button,"geometry"));

为属性添加动画意味着当进入状态时属性赋值不会立即显现。相反,当进入状态时动画开始播放并平稳动态的设置属性。由于我们没有设置动画的开始或结束值,这些值将被隐式设置。当动画开始播放,它的开始值为属性的当前值,结束值的设置将根据状态定义的属性赋值。

如果状态的全局恢复策略设置到了QStateMachine::RestoreProperties,,也可以为属性恢复添加动画。

DetectingThat All Properties Have Been Set In A State

当动画用来设置属性时,状态不再定义当状态机处于一个给定的状态时属性的确切的值。当动画运行时,依据动画属性可以是任何潜在的值。

在有的情况下,能检测到属性被赋值为状态定义的值是有用的。

  QMessageBox*messageBox =newQMessageBox(mainWindow);
    messageBox->addButton(QMessageBox::Ok);
    messageBox->setText("Button geometry has been set!");
    messageBox->setIcon(QMessageBox::Information);
 
    QState*s1 =newQState();
 
    QState*s2 =newQState();
    s2->assignProperty(button,"geometry",QRectF(0,0,50,50));
    connect(s2, SIGNAL(entered()), messageBox, SLOT(exec()));
 
    s1->addTransition(button, SIGNAL(clicked()), s2);

当点击button ,状态机转换到状态S2,设置button的几何大小,并且弹出message box 警告用户button的几何大小被改变了。

正常情况下,当不用动画时,这将如预期运作。然而,如果为S1到S2之间的转换添加了一个动画。当进入S2动画就会开始启动,但是在动画完成之前geometry 属性还没达到定义的值。在这种情况下,message box 会在button的geometry 被设置值之前弹出。

为了确保message box 直到geometry 设置为最终的值才会弹出,我们可以用状态的propertiesAssigned()信号。当属性被设置为最终值状态才会发出propertiesAssigned()信号,无论属性的值是立即被赋值还是在动画完成后赋值。

QMessageBox*messageBox =newQMessageBox(mainWindow);
    messageBox->addButton(QMessageBox::Ok);
    messageBox->setText("Button geometry has been set!");
    messageBox->setIcon(QMessageBox::Information);
 
    QState*s1 =newQState();
 
    QState*s2 =newQState();
    s2->assignProperty(button,"geometry",QRectF(0,0,50,50));
 
    QState*s3 =newQState();
    connect(s3, SIGNAL(entered()), messageBox, SLOT(exec()));
 
    s1->addTransition(button, SIGNAL(clicked()), s2);
    s2->addTransition(s2, SIGNAL(propertiesAssigned()), s3);

在这个例子中,点击button,状态机就进入S2状态。它会一直处于S2状态直到geometry 属性设置为 QRect(0, 0,50, 50).。然后才转换到S3状态。当进入S3状态就会弹出message box。当到S2状态的转换有geometry 属性的动画,状态机将一直处于S2直到动画播放完毕。如果没有这样的动画,它将简单的设置属性并立即进入状态S3。

不管怎样,当状态机处于S3,就确保了geometry 属性已经被赋值为定义的值。

如果全局恢复策略设置到了 QStateMachine::RestoreProperties,状态将不会发出propertiesAssigned() 信号直到这些也被执行。

WhatHappens If A State Is Exited Before The Animation Has Finished

如果状态有属性赋值,而且到该状态的转换有属性的动画,在属性被赋值为状态定义的值之前,状态可能被退出。尤其是当转换是来自与不依赖于propertiesAssigned 信号的状态外部,就像前面章节讲到的一样。

状态机的API保证了状态机赋值的属性要么:

有一个值显示的赋值给该属性。

正在动画的进入一个值显示的赋值给该属性。

当状态在动画完成之前被退出,状态机的行为依赖于转换的目标状态。如果目标状态显示的赋一个值给属性,将不会执行额外的操作。属性将被赋值为目标状态定义的值。

如果目标状态没有给属性任何赋值,有两种选择:默认的,属性的将赋值为它离开的状态定义的值,然而,如果设置了全局恢复策略,这将会优先发生,属性将像平常一样恢复。

DefaultAnimations

如前所述,你可以添加动画到转换中确保属性在目标状态被动态的赋值。如果你想为某个属性指定一个动画不管执行的是哪个转换,你可以添加默认动画到状态机。这是很有用的,当属性被指定的状态赋值但是并不知道状态机是何时创建的。

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赋值的。

注意:转换显示设定的动画优先于任何默认的动画。

NestingState Machines

QStateMachine是 QState. 的子类。这使得状态机可以成为其他状态机的子状态。QStateMachine 重新实现了 QState::onEntry() 并调用QStateMachine::start(),。因此,当进入子状态机,它将自动启动。

父状态机在状态机算法中把子状态机当作原子状态。子状态机是自包含的,它维护自己的事件队列和配置。尤其注意的是:子状态机的配置不是父状态机配置的一部分。

子状态机的状态不能作为父状态机转换的目标状态,只能是子状态机本身的目标状态。相反的,父状态机的状态也不能作为子状态机的转换的目标状态。子状态机的 finished() 信号可以用来触发父状态机的转换。

http://blog.csdn.net/hai200501019/article/details/9316415

Qt状态机框架的更多相关文章

  1. Qt 状态机框架学习(没学会)

    Qt状态机框架是基于状态图XML(SCXML) 实现的.从Qt4.6开始,它已经是QtCore模块的一部分.尽管它本身是蛮复杂的一套东西,但经过和Qt的事件系统(event system).信号槽(s ...

  2. Qt状态机框架(状态机就开始异步的运行了,也就是说,它成为了我们应用程序事件循环的一部分了)

    状态机框架 Qt中的状态机框架为我们提供了很多的API和类,使我们能更容易的在自己的应用程序中集成状态动画.这个框架是和Qt的元对象系统机密结合在一起的.比如,各个状态之间的转换是通过信号触发的,状态 ...

  3. 如何保证Qt状态机的最佳性能

    如何保证Qt状态机的最佳性能 How to ensure the best Qt state machine performance 如果您使用Qt进行应用程序开发,并且使用状态机,那么很可能您正在使 ...

  4. QT皮肤框架-TQUI

    本皮肤框架的相关文档,请在附件中下载,包括测试程序源码,帮助文档.相关文档可到我的百度网盘中下载,或者在本贴附件中下载. 百度网盘地址:TQUI-V1.0项目说明及测试程序源码 项目更新说明:---- ...

  5. Qt 动画框架

    最近一个项目中的需要做动画效果,很自然就想起来用qt animation framework .这个框架真的很强大.支持多个动画组合,线性动画,并行动画等.在此总结一下使用该框架一些需要注意的地方: ...

  6. Qt动画框架The Animation Framework

    动画框架是Kinetic(运动)项目的一部分,它的目标是提供一中简单的方法创建动画的和流畅的GUI.借助Qt动画属性,可以提供非常自由的动画窗体组件和其他对象(QObjects).动画框架也能被用于图 ...

  7. QT显示框架嵌入Vs控制台工程

      一.一些准备工作: 1.安装Qt for VS 的插件: 安装Qt for VS 的插件 下载地址:http://download.qt.io/official_releases/vsaddin/ ...

  8. QT状态机

    首先吐槽下网上各种博主不清不楚的讲解 特别容易让新手迷惑 总体思想是这样的:首先要有一个状态机对象, 顾名思义,这玩意就是用来容纳状态的.然后调用状态机的start()函数它就会更具你的逻辑去执行相关 ...

  9. Qt基本框架介绍

    #include <QApplication>#include <QWidget> int main(int argc, char *argv[]){ QApplication ...

随机推荐

  1. .net 中实现php rawurlencode方法(RFC3986)

    在对接api时候,经常需要对字符串进行各种编码处理,系统可能是异构的,实现语言也就可能是不一样的.所以经常会使人犯一些2B的错误! 比如:php实现的api中用了rawurlencode,文档中写,需 ...

  2. android设置按钮按下的不同效果图

    <!-- 按钮设置按下去的不同效果的方式,设置android:background属性, 下面的 button_select实际上是button_select.xml --> <Bu ...

  3. base64这种编码的意义

    BASE64不是用来加密的.你看看经过BASE64编码后的字符串,全部都是由标准键盘上面的常规字符组成,这样编码后的字符串在网关之间传递不会产生UNICODE字符串不能识别或者丢失的现象.你再仔细研究 ...

  4. judge loop in undirected graph

    一 深度优先遍历,参考前面DFS(white and gray and black) 二 根据定点以及边数目进行判断 如果m(edge)大于n(vertex),那么肯定存在环 算法如下: 1 删除所有 ...

  5. 安装CDH

    Cloudera Manager 4.8.2 http://www.idefs.com/recordsubuntu-12-04-cloudera-installation-manager-4-8-2- ...

  6. sortable.js 华丽丽的排序

    首先导入这几个资源 <link href="/css/jquery-ui-1.10.3.custom.css" rel="stylesheet" type ...

  7. LINUX下使用crontab进行RMAN备份实验

    之前写了脚本,手动执行可以,使用crontab总是无法运行成功,今天下午花了两个小时实验,完成如下: 注意事项:脚本完成首先手动执行,确定可以正常执行. 在crontab中使用,要注意以下几点: 1. ...

  8. kinect for windows - 环境搭建

    我是在虚拟机上搭建的开发环境,需要准备如下软件: 1)vmware workstation 10.0.2 (可以去官网下载,key就自己百度吧) 2)win7 32位(一定是32位的) 3)vs201 ...

  9. java学习之网络编程之echo程序

    服务端的实现 package com.gh.echo; import java.io.*; import java.net.*; /** * echo服务器程序 * 实现 不断接收字符串 ,然后返回一 ...

  10. H面试程序(15): 冒泡排序法

    #include<stdio.h> #include<assert.h> void display(int * a, int n) { for(int i = 0; i < ...