(相关的代码可以从https://github.com/goldenhawking/mercator.qtviewer.git直接克隆)
本文的前序章节介绍了坐标系的基础知识。在这一章,我们将进行架构设计。架构是一个软件生命体的骨骼,为了实现灵活的功能扩展,首先要引入插件机制。
鉴于 Qt 框架本身提供了良好的面相对象插件接口开发能力,不妨就利用这个机制来实现我们的意图。草图如下:
* 瓦片地图是一个视图,叫做 tilesviewer。理想情况下,它只负责以下几个事情:
(1)坐标转换。不但提供上一篇文章中所提坐标系、经纬度坐标系、墨卡托坐标之间的方便转换,而且记录当前可视区域大小、中心位置、比例尺。 图层可以在需要的时候获取这些参数,或者根据自身需求改变他们(一般是应用户的拖动等)。换句话说,主框架是被动的,如何响应用户的鼠标消息,由图层决定。这也就允许图层采取不同的行为,比如拉框放大或者漫游。
(2)消息传递。提供一个服务接口,允许图层发送消息给主框架、其他图层甚至是外部程序(如ActiveX宿主)。
(3)刷新控制。允许图层发出消息刷新主框架。
这些功能都由一个接口类 viewer_interface发布。由于tilesviewer是一个简单的widget,本身没有缩略图以及比例尺框等辅助工具,我们提供一个容器widget来管理布局,这个容器widget叫做osm_frame_widget。很多界面功能,如双屏显示、停靠、隐藏工具栏等等都在这里实现。
*图层是一个带有属性页面的功能类,负责实现具体的行为。如果这个图层在外部DLL中实现,则叫做插件。
(1)回调响应。主框架会在鼠标事件、绘图事件等UI相关事件中回调各个图层的接口。图层需要按照自己的功能要求响应这些回调,并完成功能。
(2)管理消息。图层可以发送消息给主框架、其他图层甚至是外部程序(如ActiveX宿主)。同时,需要有一个接口来接受外部的消息,并作出响应。
(3)功能调用。图层可以通过一套 key-value 接口发布自己的功能。比如,标绘插件支持从外部送入命令,产生标记。在Qt框架内部,这种调用是基于 QMap<QString, QVariant> 参数的,这是一种哈希表 key-value 存储。调用者可以是其他图层或是外部程序(ActiveX宿主)。
在主框架内,提供了两个图层,一个是layer_browser,用于显示缩略图。一个是 layer_tiles,是OSM瓦片的背景视图,支持简单的显示、拖动漫游。图层有几个属性需要注意:
(1)可视属性。visible 属性决定了图层是否可见。图层实现者需要注意,是否可见应该仅仅决定本图层是否参与绘图,而与消息响应无关。
(2)活动属性。active 属性决定了图层是否响应消息。图层实现者需要注意,是否活动应该仅仅决定本图层是否响应消息,而与参与绘图无关。
(3)排他属性。exclusive属性决定了图层是否独享消息。在同一个时刻,所有标记为排他的图层中,只有一个图层可以为活动的。这个行为不影响非排他视图。这个属性非常关键,图层实现者需要根据功能决定本图层是否排他。比如,一个支持拉框选取要素的图层,和一个支持漫游的图层之间应该是互斥的,要不然一拉框,地图就跟着动弹了。又比如,一个绘制经纬度分划线的图层可以是非排他的,这样就可以时刻响应鼠标消息,提示用户鼠标在哪里。
在windows 下,activeX控件是向本地程序发布功能的较方便的技术。如果想让地图方便的嵌入到 C#, VB等开发的宿主上,ActiveX很不错(当然Web也是可以的,而且在今后是个趋势)。Qt进行 ActiveX封装非常方便,只需要从 QAxBindable派生出一个包装类即可。但是,有一个潜在的问题需要格外小心:
* Qt 插件系统的静态成员。Qt 插件系统将功能发布到动态链接库后,可通过QPluginLoader加载。一般,无论调用多少次load,只有一个插件实例被创建。这也非常容易理解,因为无论一个进程调用了多少次 load,DLL在内存里其实只有一份。这样的副作用是当 ActiveX宿主拥有两幅地图控件,他们加载插件时,其实只创建了一个实例。按照我们的功能需求,每幅地图的行为应该是独立的,因此,需要手工的维护一套机制,以保证为每幅地图创建一个新的插件实例。最简单的方法就是自举工厂——一个创建同类型对象的方法。只要安排一个机制,在插件被调入时,立刻调用这个工厂,并返回与主框架视图对应的插件实例即可,代码如下:
首先,在全局静态资源内安排一个映射:
/*! * The plugin dynamic library (.dll in windows or .so in linux) will be loaded into memory only once. * for example, a windows app like test_container will contain 2 qtaxviewer_planetosm OCX ctrls , * each OCX ctrl will load plugins when initializing. We need a mechanism to handle this situation, * that maintains a connection between plugin instances and their parents(always be viewer_interface). * For the reason above, these paraments are introduced: */ QMutex mutex_instances; //!This QMutex protect map_instances and count_instances QMap<viewer_interface *, qtvplugin_grid * > map_instances; //!Mapping viewer_interface to qtvplugins QMap<QString, int > count_instances; //!a counter for instances numbering这个映射记录了视图与插件的对应关系。当然,设置一个mutex保护可能是没有必要的,但保护起来也是一个很好的习惯。
而后,在工厂方法中,根据视图对象实例来决定是否要创建新的插件类对象,或者直接返回现有的对象。
layer_interface * qtvplugin_grid::load_initial_plugin(QString strSLibPath,viewer_interface * ptrviewer) { //!In this instance, we will see how to create a new instance for each ptrviewer qtvplugin_grid * instance = 0; //!1.Check whether there is already a instance for ptrviewer( viewer_interface) mutex_instances.lock(); if (map_instances.empty()==true) { map_instances[ptrviewer] = this; instance = this; } else if (map_instances.contains(ptrviewer)==false) { //Create a new instance for ptrviewer instance = new qtvplugin_grid; map_instances[ptrviewer] = instance; } else instance = map_instances[ptrviewer]; mutex_instances.unlock(); //!2.if the instance correspones to this, do init operations. if (instance==this) { QFileInfo info(strSLibPath); m_SLLibName = info.completeBaseName(); m_pVi = ptrviewer; mutex_instances.lock(); m_nInstance = ++count_instances[m_SLLibName]; mutex_instances.unlock(); loadTranslation(); } //!3.if the instance not correspones to this, call the instances' load_initial_plugin instead. else { layer_interface * ret = instance->load_initial_plugin(strSLibPath,ptrviewer); assert(ret==instance); } return instance; }