先决条件
介绍
在这个教程中,我会介绍给你OGRE中最基本的结构: SceneManager, SceneNode, 还有Entity 对象.我们不会涉及大量的代码,而是观注初学OGRE的一些一般概念.
当你在阅读的过程中,你会慢慢地在你的工程中加入代码并且观察生成的结果.没有比实际的编程更好的方法来熟悉这些概念!所以不要只阅读不动手.
开始
最初的代码
在这个教程里我们会用到一个预先构造的代码结构.除了我们将要加入createScene 方法的,你应该忽视其他的代码. 在随后的教程中,我们会深入地讲解OGRE是怎么运行的,但是现在我们要从最基本的层次开始.用你选择的环境创建一个工程,并加入下面的代码:
#include "ExampleApplication.h"
class TutorialApplication : public ExampleApplication
{
protected:
public:
TutorialApplication()
{
}
~TutorialApplication()
{
}
protected:
void createScene(void)
{
}
};
#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32
#define WIN32_LEAN_AND_MEAN
#include "windows.h"
INT WINAPI WinMain( HINSTANCE hInst, HINSTANCE, LPSTR strCmdLine, INT )
#else
int main(int argc, char **argv)
#endif
{
// Create application object
TutorialApplication app;
try {
app.go();
} catch( Exception& e ) {
#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32
MessageBox( NULL, e.getFullDescription().c_str(), "An exception has occurred!", MB_OK | MB_ICONERROR | MB_TASKMODAL);
#else
fprintf(stderr, "An exception has occurred: %s ",
e.getFullDescription().c_str());
#endif
}
return 0;
}
当这个程序运行的时候,用WASD来进行移动,鼠标进行视角的旋转,ESC键退出(好像CS啊).
问题分析
Message Box 问题
如果你使用代有Unicode支持的VisualStudio,那么可能会在MessageBox()这里报错,解决办法有两种:一是把MessageBox改为MessageBoxA,另一种就是在工程设置里去除Unicode支持.
缺少配置文件或DLL
如果你在运行刚编译好的程序的时候出现缺少DLL或配置文件(*.cfg),确定你设置好环境路径 [ProjectFolder]/bin/release, [ProjectFolder]/bin/debug.
资源或插件问题
确定plugins.cfg和resources.cfg跟可执行文件在同一目录下(或在bin目录下). Plugins.cfg告诉OGRE 哪个渲染库可用 (Direct3D9, OpenGL等). Resources.cfg是 ExampleApplication 的资源配置文件,里面有textures, meshes 和 scripts的路径. 这两个都是文本文件,所以编辑它们并保证路径的正确性. 否则你的OGRE设置对话框会没有任何渲染方式可选,或者你会在屏幕上或Ogre.log中看到:
Description: ../../Media/packs/OgreCore.zip - error whilst opening archive: Unable to read zip file
|
如果发生这种情况,打开resources.cfg 文件并把路径指向OGRE的Media文件夹. 注意你不能使用任何环境变量,如 $(SomeVariable).
在Visual Studio中不能启动应用程序
一般情况是环境配置问题,使得有些文件找不到所致.详细解决办法不在本文讨论.
Ogre怎样工作
这是一个主要的话题. 我们从SceneManagers开始直到Entities和SceneNodes.这三个类是构建Ogre应用程序的基础.
SceneManager 基础
所有出现有屏幕上的东西都是由SceneManager(场景管理器)来管理的 (想像一下). 当你在场景中放置物体的时候, SceneManager记录了它们的位置. 当你创建Camera来观察场景 (我们会在后面的教程中提到)时, SceneManager 记录了它们. 当你创建plane, billboard, light...等等时,SceneManager 也记录了它们.
Entity 基础
一个 Entity(实体)是一种你能够在场景中绘制的对象. 你可以把Entity想像成3D mesh的替代品. 一个机器人是一个entity, 一条鱼是一个entity, 你的角色走在的地形也是一个巨大的entity. Light(灯光), Billboard(公告牌), Particle(粒子), Camera(摄像机)等就不是entity 了.
一个在Ogre中需要注意的问题是它把可渲染的对象跟它们的位置与方向进行了分离. 这意味着你不能直接把一个Entity放到场景中去. 所以你必须把Entity 附属于一个SceneNode 对象, 这个SceneNode包含了位置跟方向的信息.
SceneNode 基础
上面已经说过, SceneNode(场景结点) 记录了所有附属于它的物体的位置和方向. 当你创建一个Entity的时候,在你把它绑定在一个SceneNode之前,它是不会被绘制的. 相似的, 单独一个SceneNode 不是可以在屏幕上显示的物体. 只有你创建 了一个SceneNode并且在上面绑定一个Entity (或其他对象),它才能真正地在屏幕上显示出来.
SceneNode可以绑定很多个对象. 比如说你有一个角色在屏幕上走动,而且你希望他周围有灯光. 实现这个的方法是你首先创建一个SceneNode, 再创建一个角色Entity 并绑定在SceneNode上. 接下业你要创建一个Light对象并把它绑定于SceneNode. SceneNode也加以绑定在另外的SceneNode上,这样就使我们可以创建有层次关系的结点了. 我们会在之后的教程中涉及SceneNode的高级配属方式.
U关于ScenenNode 有一个重要的方面要注意,就是
SenceNode的位置总是相对于它的父级SceneNode的, 并且每个SceneManager 都包含一个根结点来绑定所有其他的SceneNode.
你的第一个Ogre应用程序
现在回到我们之前创建的代码. 找到TutorialApplication::createScene 成员函数. 在这次的教程中我们只会利用这个函数的内容. 要做的第一件事就是为场景设置环境光源使我们能看到我们在做的事情. 我们可以调用
setAmbientLight 函数并指定颜色来完成这件事. 注意ColourValue的构造函数期望的红绿蓝各分量的取值范围是0到1. 在createScene中加入这一行:
mSceneMgr->setAmbientLight( ColourValue( 1, 1, 1 ) );
接下来的我们需要做的是创建一个Entity. 我们可以调用SceneManager的 createEntity成员函数:
Entity *ent1 = mSceneMgr->createEntity( "Robot", "robot.mesh" );
好了,出现了几个问题. 首先, mSceneMgr是从哪里来的?还有我们调用的函数的参数是什么? mSceneMgr变量包含在当前的SceneManager对象中 (继承自ExampleApplication 类). createEntity的第一个参数是我们创建的Entity的名字. 所有entity必须有一个唯一的名字. 如果你尝试创建两个同名的entity,你会收到一个错误. "robot.mesh"参数指定了我们为Entity使用的mesh(网格). 同样的, 这个网格已经被ExampleApplication类预先加载了.
既然我们创建了一个, 我们需要创建一个SceneNode来绑定. 因为每个SceneManager都有一个根SceneNode, 我们要创建那个结点的子结点:
SceneNode *node1 = mSceneMgr->getRootSceneNode()->createChildSceneNode( "RobotNode" );
这个长长的语句首先调用了当前SceneManager的getRootSceneNode. 然后又调用了SceneNode的createChildSceneNode方法. createChildSceneNode 的参数是我们创建的SceneNode的名字. 就像Entity类一样, 两个SceneNodes 不能有相同的名字.
最后, 我们需要把Entity 绑定在SceneNode上来使机器人有一个绘制的位置:
node1->attachObject( ent1 );
好了! 编译并运行你的程序. 你应该在屏幕上看到一个站立的机器人.
注意: Robot.mesh 不在OgreCore.zip中. 跟着这个教程来做到这个时候,你的程序可能能运行但没有显示任何东东. 我发现在resources.cfg 加入适当的路径会使程序正常运行.
FileSystem=../../media/materials/programs
FileSystem=../../media/materials/scripts
FileSystem=../../media/materials/textures
FileSystem=../../media/models
|
坐标系和向量
在我们深入之前, 我们需要说一下屏幕坐标系和Ogre向量对象. Ogre (像很多其他图形引擎一样) 使用x和 z轴来做为水平面, y轴做为垂直轴. 当你看向屏幕的时候, x轴应该是从左到右的, 并且右侧是x轴的正方向. Y轴在屏幕是从下到上的,上面那一端是正方向. Z轴是从里到外的,屏幕外的这一端是正方向.
为什么机器人面朝x轴轴的正方向呢? 那是网格自己的属性,因为原来它就是那个样子. Ogre 并没有设定你的模型的朝向.你加载的每个网格都会有不同的方向.
Ogre 使用Vector类来表示位置和方向(没有Point类). 向量有2 (Vector2), 3 (Vector3), 还有 4 (Vector4) 维的, 其中Vector3是最常用的. 如果你对向量不熟悉的话, 我建议你在认真学习Ogre之前复习一下. 与向量相关的数学知识在你进行复杂程序的时候是非常有用的.
加入其它的物体
现在你理解了坐标系统,我们可以回到我们的代码上来了. 在我们写的那三行代码中,并没有设置机器出现的位置. 绝大多数Ogre的函数都有参数的默认值. 例如,Ogre中的
SceneNode::createChildSceneNode 成员函数有3个参数: SceneNode的名字, SceneNode的位置, 还有初始的方向. 就像你看到的一样,位置已经被设为 (0, 0, 0)了. 我们来创建别一个SceneNode, 但是这次我们指定位置:
Entity *ent2 = mSceneMgr->createEntity( "Robot2", "robot.mesh" );
SceneNode *node2 = mSceneMgr->getRootSceneNode()->createChildSceneNode( "RobotNode2", Vector3( 50, 0, 0 ) );
node2->attachObject( ent2 );
这看起来很眼熟. 我们跟之前做了一件相同的事, 除了两点. 首先, 我们对Entity 和SceneNode的命名有点差别. 第二点就是我们指定位置在x轴离根SceneNode 50单位的距离上 (记住所有的SceneNode的位置是相对于父结点的). 编译并运行这个demo. 现在有两个并排的机器人了.
深入了解Entity
Entity类的应用非常广泛,这里我不会涵盖所有的使用方法...仅仅够你入门就好了. 我愿意指出在Entity中有一些可以直接使用的函数.
首先是
Entity::setVisible 和
Entity::isVisible. 你可以通过调用这个函数来设置任何一个Entity可见. 如果你想暂时隐藏一个Entity,这时候调用这个函数比销毁它再重建要好得多. 注意你不需要缓存这些Entity. 每个物体的网络和纹理只会载入内存一次,所以当你试着保存它们的时候并没有省下多少资源. 唯一节省的是你创建和销毁Entity对象所花费的那一点点时间而已.
深入了解SceneNode
SceneNode 非常复杂. 很多事情都可以通过SceneNode来完成,但我们只涉及最常用的那些.
因为所有的位置和移动都是相对于父结点的, 我们可以非常方便地一块儿移动两个SceneNode.我们现在程序中有这些代码:
Entity *ent1 = mSceneMgr->createEntity( "Robot", "robot.mesh" );
SceneNode *node1 = mSceneMgr->getRootSceneNode()->createChildSceneNode( "RobotNode" );
node1->attachObject( ent1 );
Entity *ent2 = mSceneMgr->createEntity( "Robot2", "robot.mesh" );
SceneNode *node2 = mSceneMgr->getRootSceneNode()->createChildSceneNode( "RobotNode2", Vector3( 50, 0, 0 ) );
node2->attachObject( ent2 );
如果我们把第5行从:
SceneNode *node2 = mSceneMgr->getRootSceneNode()->createChildSceneNode( "RobotNode2", Vector3( 50, 0, 0 ) );
改成这样:
SceneNode *node2 = node1->createChildSceneNode( "RobotNode2", Vector3( 50, 0, 0 ) );
这样我们就把RobotNode2变为 RobotNode的子结点了. 移动node1 时node2也会随着移动, 但是移动node2不会影响到node1. 例如这些代码可以只移动RobotNode2:
node2->translate( Vector3( 10, 0, 10 ) );
下面的代码会移动RobotNode, 同时由于RobotNode2 是RobotNode的子结点, RobotNode2也会移动:
node1->translate( Vector3( 25, 0, 0 ) );
如果你弄不清, 最简单的方法就是从根结点开始向下. 像这样, 我们把node1 从 (0, 0, 0)开始移动 (25, 0, 0), 从而node1的位置相对于父级变为 (25, 0, 0). node2从 (50, 0, 0)开始移动(10, 0, 10), 所以它相对于父级的新位置是(60, 0, 10).
现在我们计算出这些东西真正的位置. 从根SceneNode开始. 它是位置总是(0, 0, 0). 现在, node1的位置是(root + node1): (0, 0, 0) + (25, 0, 0) = (25, 0, 0). 这并不意外. node2 是node1的子结点, 所以它的位置为(root + node1 + node2): (0, 0, 0) + (25, 0, 0) + (60, 0, 10) = (85, 0, 10). 这也是一个解释SceneNode的层级结构的好例子. 你很少会需要计算你的结点的绝对位置的.
最后, 注意你可以通过调用SceneManager 的
getSceneNode 和
getEntity 方法得到SceneNode和Entity, 所以你不必为你创建的每个SceneNode保存一个指针. 但你可以保存你最常使用的那个.
尝试一下
到现在你应该基本掌握了Entitiy, SceneNode, 和SceneManager了. 我建议以上面的代码为基础在场景中加入或删除机器人. 当你完成的时候,清空所有createScene方法中的内容, 并玩玩下面的代码段:
缩放
你可以通过调用SceneNode中的scale方法来缩放网格. 试着更改scale的参数值来看一下你能得到什么:
旋转
你可心用yaw,pitch和roll方法来旋转对象,并且roll方法可以用度或者弧度单位. Pitch是绕x轴的旋转, yaw是绕y轴的旋转, roll是绕z轴的旋转. 用你的右手来做一下示范: 把你的拇指、食指跟中指各成90度,然后拇指向右为x轴,做一下各种旋转试一下.
试着改变角度,并结合多种变换:
Ogre环境
在这个教程中引用的大多数的文件 (.DLL and .CFG) 可以在OgreSDK 的"bin" 下的debug 或 release中找到.你创建的 Debug 程序应该使用debug文件夹中的文件, release程序应该使用release文件夹中的.
注意这里讨论了许多特定的Windows下的术语. Linux下也一样,不过共享库文件以.so结尾并在其它的地方,有些东西会有细微的不同. 如果你有问题,可以发布到Ogre论坛help版块上去.
DLL和插件
现在我们已经接触了一些Ogre环境, 我愿意大体上讲解一下Ogre库是怎么工作的, 还有怎么更方便的去应用它.
Ogre被分为几组共享的库文件. 第一组是链接库本身还有它依赖的库. Ogre库 在OgreMain.dll中包含着. 这个dll需要一些其它的链接库,如cg.dll. 这些DLL必须毫无例外地包括在每个Ogre应用程序中.
第二组链接库是插件. Ogre把各种功能合理地分配到链接库中去,因些它们可以根据你的程序的需要来加入. Ogre中基本的插件的文件名以"Plugin_" 开头. 如果的程序需要的话,你可以创建新的插件,但是我们不会在教程中涉及这个. Ogre同样使用插件来进行渲染(如OpenGL, DirectX, 等).这些插件以"RenderSystem_"为前缀. 它们已经可用了,你可以在你的程序中加入或移除它们.当你写OpenGL着色器或特殊应用的并且希望关闭DirectX下运行的能力的时候,这是非常有用的, 你可以简单地移除对应的RenderSystem插件,这样它就会不可用了. 另外,如果你的目标平台是非标准的, 你可以编写你自己的RenderSystem 插件, 但这个会不在教程中涉及到. 我们会在下一个部分提到怎样移除插件.
第三组链接库是第三方库和其它有所帮助的库. Ogre它本身仅仅是一个图形渲染库. 它并没有包含一些东西,如GUI 系统, 输入控制, 物理引擎, 等等. 你必须用其它的链接库来做这些事情. Ogre demos和SDK包含了一些这样的第三方库. CEGUI 库是一个可以方便地与Ogre进行整合的GUI 系统, 它的DLL以 "CEGUI*"开头并且"OgreGUIRenderer.dll"是它的一部分. CEGUI的使用会在之后的教程中涉及到. 键盘和鼠标输入是通过OIS(一个输入系统)来完成. 它包含在OIS.dll中. 还有一些库(没有包含在SDK中)可以提供更多的功能 (如声音和物理引擎),你可以在Wiki和论坛上找到更多的信息.
这些话的用意是当你在本也测试你的程序时, 你可以把所有的东西都"打开" 就是不要移除任何东西). 当你准备发布的你的程序时,你需要在Release模式下生成它,包含所以你的程序用到的release DLL, 并且移除你用不到的DLL. 如果你的程序没有使用CEGUI但是用到了OIS, 那么你不应该包含CEGUI的DLL, 但是必须包含OIS dll,否则你的程序不会正常运行
配置文件
Ogre 有许多配置文件. 它们控制加载哪些插件, 从哪里加载程序资源, 等等. 我看来简单地看一下各个配置文件是干什么的. 如果你有更多的细节问题,你应该直接去Ogre的帮助论坛.
plugins.cfg这个文件包含了你的程序使用的插件. 如果你想加入或去除某个插件, 你需要编辑一下这个文件. 要去除一个插件, 只删除对应的行或在开头加#注释掉就好了. 要加入一个插件的话, 你需要像这样加入一行:"Plugin=[PluginName]". 注意别忘了在插件名称的结尾加上 .DLL. 你的插件并不强制以"RenderSystem_" 或 "Plugin_"开头. 你也可以通过更改"PluginFolder"变量定义Ogre寻找插件的目录. 你可以使用绝对或相对路径, 但是不能使用环境变量,如$(SomeVariable).
resources.cfg这个文件包含了Ogre查找资源的路径列表. 资源包括脚本, 网格, 纹理, 等等. 你可以使用绝对或相对路径, 但是不能使用环境变量,如$(SomeVariable). 注意Ogre不会去扫描子目录,所以如果你有多级目录的话,你必须手动的加入它们. 例如, 你有这样的一个目录树,"res/meshes" 和 "res/meshes/small", 你必须在配置文件加入两行包含这些路径的条目.
media.cfg这个文件告诉Ogre 更详细的资源信息. 好像现在你没必要编辑这个文件, 所以我将跳过这些细节. 更多的信息可以在用户手册和Ogre论坛上找到.
ogre.cfg这个文件是由Ogre的配置对话框生成的. 这个文件可以设置你的计算机和图形选项. 你不应该在共享你的程序的时候把这个文件发行给其他人, 因为他们的设置可能会跟你不同. 注意你不该直接编辑这个文件,而是应该使用配置对话框.
quake3settings.cfg这个文件是由BSPSceneManager使用的. 你不需要这个文件,除非你用到了这个scene manager (现在你就没有用到), 所以忽略它. 你不该发由这个文件,除非,你使用了BSPSceneManager, 即使那时你的程序决定了它可能跟现在完全不同.
上面这些就是Ogre直接操作的配置文件. Ogre必须能够找到"plugins.cfg", "resources.cfg", 和 "media.cfg" 才能正常运行. 在后面的教程中我们会涉及到怎么改变这些文件的位置并操作他们完成更多高级功能.
更好的方法来测试你的程序
注意这部分是关于Windows和Visual C++的(xoyojank:所以我只大体提一下吧)
上面说过你要把bin文件夹的路径加入工程设置,不然程序会找不到DLL之类的文件.比起每次都把DLL拷贝到程序目录下来说,直接复制到Windows/System32/下更方便一些. (略)
总结
到这里你应该对SceneManager, SceneNode, 和 Entity 类有个初步的掌握了. 你不必熟悉所有我介绍过的函数. 既然它们都是最基本的对象, 我们会经常使用的. 你会在接下来的教程中对它们更加了解的.
你也应该对配置Ogre环境有所熟悉了.
终于翻译完啦.
其实OGRE 3D 中文(
http://ogre3d.cn)上已经有人翻译过了,只是去掉了一些东西.我翻译这个,也是想对自己有所锻炼吧.希望继续学习的,可以参照:
如果你用的是Windows下的OgreSDK, 一定要把这个目录 "[OgreSDK_DIRECTORY]/samples/include" 加入工程的包含路径( ExampleApplication.h 文件从这个位置加载). 如果用的是OGRE的源代码版本,你需要包含"[OgreSource_DIRECTORY]/Samples/Common/include" 目录. 确认你在继续下一部分之前你可以编译并运行这些代码,虽然除了一个有帧速度的框的空白屏幕外没有任何东西.我们会在这个教程后面加入一些能够显示的对象.
- Zipline Beginner Tutorial
Zipline Beginner Tutorial Basics Zipline is an open-source algorithmic trading simulator written in ...
- Axiom3D学习日记 2.介绍SceneManager,SceneNode,Entity
SceneManager(场景管理类) 所有出现在屏幕里的东西都受SceneManager管理(最好是这样),当你放置对象在场景里,SceneManager就会跟踪他们的位置,当你为场景创建一个相机, ...
- 【转载】C# Tutorial - Simple Threaded TCP Server
http://tech.pro/tutorial/704/csharp-tutorial-simple-threaded-tcp-server In this tutorial I'm going t ...
- [转载] CMake Official Tutorial——教程还是官方的好
CMake官方教程传送门:https://cmake.org/cmake-tutorial/ 以下的内容跟官方教程基本一致,少数地方根据自己的测试有所改动: A Basic Starting Poin ...
- 【转载】Pytorch tutorial 之Datar Loading and Processing
前言 上文介绍了数据读取.数据转换.批量处理等等.了解到在PyTorch中,数据加载主要有两种方式: 1.自定义的数据集对象.数据集对象被抽象为Dataset类,实现自定义的数据集需要继承Datase ...
- OGRE启动过程详解(OGRE HelloWorld程序原理解析)
本文介绍 OGRE 3D 1.9 程序的启动过程,即从程序启动到3D图形呈现,背后有哪些OGRE相关的代码被执行.会涉及的OGRE类包括: Root RenderSystem RenderWindow ...
- OGRE 1.9 的第一个程序(OGRE HelloWorld程序)
平台:win7, VS2010 先看运行结果吧: 1. 安装OGRE 下载OGRE SDK 1.9,解压,放在你喜欢的地方,在OGRE SDK文件加下创建“OGRE_HOME.bat.bat”文本文件 ...
- OGRE
[编译 OGRE C#] 1.官网下载 OGRE 源码. 2.打开CMake,指定好OGRE目录,以及生成目标目录. 3.将 CMake/Modules 目录下的 FindSwig.cmake 中的 ...
- [Ogre][地形][原创]基于OgreTerrain的地形实现
需要用到的外部图片资源: 在ogre调用时需要多用到的几个外部dll: OgreTerrain_d.dll 需要添加头文件 #include "Ogre\Ogre.h"#inclu ...
随机推荐
- 智慧监狱来了!SaCa EMM 助推现代监狱建设迈上新台阶
近几年来,移动化已经成为警务信息化建设的必然方向,为紧急和突发事件的处理提供了信息依据.为监狱民警提供移动警务所需的信息管理系统,司法系统从很早就开始推动警务通项目.为了落实移动警务的工作需求,很多监 ...
- 如何批量下载bing的背景图片?
工具准备 wget(点击下载) 批处理命令(点击下载) 网友提供的接口:http://area.sinaapp.com/bingImg?daysAgo=1(1代表天数) 实现步骤 1.打开记事本,并将 ...
- EF直接的一對多多對多多對一的關係----也即是鏈錶查詢
基于EF4.1 code first 简单的CRUD 园子中已经有很多了 ~~ 真不想再写这个了 可是为了做一个完整的小demo 从开始 到后面的一些简单重构 还是决定认真把这个写出来 争取写些别人 ...
- SVN篇
启动SVN : svnserve -d -r svn 查看进程: ps -ef | grep svmserve -------------------------------------------- ...
- 【Linuc-CentOS 】通过yum安装 指定版本的nodejs
原 [Linuc-CentOS ]通过yum安装 指定版本的nodejs 2018年06月21日 06:56:32 黑夜的风 阅读数:884 版权声明:本文为博主原创文章,未经博主允许不得转载. ...
- 使用XWAF框架(5)——XML解析器:CXDP
XWAF推出了自己的组合式XML文档解析器,英文名叫:“CXDP”,是“Combined XML Document Parser”的缩写.核心代码属XWAF原创,注释.日志和帮助文档采用全中文描述,特 ...
- Set集合之HashSet类
HashSet简介 HashSet是Set接口的典型实现,大多数时候使用Set集合时就是使用这个实现类.HashSet按Hash算法来存储集合中的元素,因此具有良好的存取和查找性能. HashSet特 ...
- Web | Webpack快速上手
概述 Webpack 是一个前端资源加载/打包工具.它将根据模块的依赖关系进行静态分析,然后将这些模块按照指定的规则生成对应的静态资源. 一般我们在开发中都是使用终端去进行安装,使用npm,关于npm ...
- P1379 八数码难题
#include<iostream> #include<bits/stdc++.h> using namespace std; #include<vector> u ...
- 可编辑div中包含子元素时获取光标位置不准确的问题
前言: 高亮显示输入框中的关键字符,这就必须得用到可编辑div(或其他标签)元素了,这时我们需要获取光标的位置,以便插入字符. 正文: 正常情况下获取光标位置,代码如下: function getPo ...