使用 C++ ATL/WTL (Windows Mobile 6, Standard) 进行 GPS 和 Web 服务






4.93/5 (25投票s)
本文介绍了如何在智能手机上使用 GPS API 和 Web 服务来显示当前位置的人口统计信息。
目录
引言
有很多关于如何在托管环境中(managed world)使用 Web 服务的示例,但有时,安装完整的运行时(runtime)是一种不必要的奢侈。如果你想要轻量级的解决方案,本文将让你一窥如何实现这一点。
在过去的几年里,Web 服务开发一直是我主要关注的领域之一。我设计并开发了各种 Web 服务以及消耗 Web 服务的客户端应用程序。我使用过不同的环境/语言来构建基于 Web 的客户端以及独立的桌面应用程序。我之前没有尝试过的是在移动设备上构建 Web 服务客户端应用程序。这也是我决定为本文构建一个移动应用程序的原因之一。我还想做一些比股票行情提示应用或亚马逊商品搜索应用更有趣的事情,这些应用可以利用其托管设备的功能。幸运的是,我刚收到了一部新的支持 GPS 的智能手机,MOTO Q 9h。在手机上拥有 GPS 是我选择构建移动应用程序示例的第二个原因,因为我有一个不错的 Web 服务来完善这个图景,下面将进行描述。
背景
演示应用程序已在 MOTO Q 9h 智能手机上进行了测试。该手机的屏幕尺寸为 320x240。演示应用程序不支持方向和/或尺寸的自适应。它使用静态的 320x240 位图作为背景图像。但是,让它支持方向/尺寸自适应并不难。
使用代码
请确保您已阅读本文并准备好所有必需品。下载、解压缩、编译、部署,然后尽情享受吧!我还包含了一个预编译的 CAB 文件。
ATL, WTL
我将使用 ATL 库,它内置了对 Web 服务(Web Services)的支持。对于用户界面,我将使用我最喜欢的之一——Windows Template Library (WTL)。WTL 是 ATL 的轻量级扩展。WTL 提供了基于模板的 Windows 控件和各种 Win32 API 的封装。WTL 可以在此处下载。本文使用的是 WTL 8.0,它与 Visual Studio 2005 集成得非常好。下载并安装 WTL 后,请从 `AppWizMobile` 文件夹运行 `setup80.js`。该脚本将为 Visual Studio 2005 安装应用程序向导。
Application
以下部分提供了有关如何创建应用程序项目和添加 Web 服务引用的分步说明。
使用 WTL 向导创建项目
让我们开始启动 Visual Studio 2005。
如果您已安装 WTL 并注册(通过运行 `setup80.js`)了 WTL 应用程序向导,则会在 Visual C++/WTL 项目类型类别下找到新的“WTL Mobile Application Wizard”模板。
为您的项目命名,选择位置,然后单击“OK”。我将我的项目命名为 GPSDemographics。WTL Mobile Application Wizard 对话框有四个页面:
1. 概述页
- 单击“Next >”。
2. 平台页
- 选择 Windows Mobile 6 Standard SDK,并从“Selected SDKs”部分删除所有其他 SDK。
- 单击“Next >”。
3. 应用程序类型页
- 选择“SDI Application”
- 勾选“Enable ActiveX Control Hosting”。此选项会在项目中设置一些与线程相关的定义,这些定义由 ATL SOAP 实现使用。不勾选此选项将导致一个错误,并解释需要设置哪个定义。
- 勾选 *Generate .cpp files*。
- 单击“Next >”。
4. 用户界面功能页
- 我们将使用 View 窗口,“Generic window”作为 View 类型。
- 单击“Finish”。
项目设置
由于我们正在使用 WTL,因此必须让 Visual Studio 知道头文件(header files)的位置。我知道您肯定知道有几种方法可以做到这一点。但以防万一,我将向您展示如何通过项目属性对话框来完成。
打开项目属性窗口。Project -> Properties。
请为以下属性添加指向您的 WTL include 文件夹的路径:
- Configuration Properties > C/C++ > General > Additional Include Directories。
- Configuration Properties > Resources > General > Additional Include Directories。
Web服务
我将使用 CDYNE 的 Demographics Web Service。该服务为给定地址提供社会经济数据。
该 Web 服务有 15 个不同的方法。我将使用 `GetLocationInformationByLatitudeLongitude` 方法,该方法以地理位置作为输入。
添加 Web 引用
确保您的 Solution Explorer 已打开。
- 在 Solution Explorer 中右键单击项目项。选择“Add Web Reference...” 。
- 在 URL 编辑框中输入 Web 服务 URL 或 WSDL URL。在我们的例子中,它是:http://ws.cdyne.com/DemographixWS/DemographixQuery.asmx。
- 单击“Go”。
如果一切正常,您将看到 Web 服务页面。在这种情况下,服务的名称是 DemographixQuery。
- 单击“Add Reference”完成。
此时,您应该能够编译项目。您可能会收到一个类似如下的错误:
此错误是由于名称冲突引起的。为了修复它,请在 `stdafx.h` 文件中 `#include "WebService.h"` 之前添加以下内容:
#ifdef lstrlenW
#undef lstrlenW
#endif
GPS
在 Windows Mobile 中处理 GPS 硬件有几种方法。在本文中,我将使用 GPS Intermediate Driver API。使用 GPS Intermediate Driver 的主要好处是它提供了一个中间层,隐藏了实际 GPS 设备的复杂性和实现细节。使用 GPS Intermediate Driver 架构的应用程序应该适用于任何 GPS 硬件。
在 GPS Intermediate Driver 下处理 GPS 硬件有两种模式:
- Parsed mode,使用 GPS Intermediate Driver API。
- Raw mode,使用 `CreateFile/ReadFile/CloseHandle`。
我将使用 Parsed mode。该 API 由四个函数组成:
GPSOpenDevice
GPSCloseDevice
GPSGetPosition
GPSGetDeviceState
`GPSOpenDevice` 是您必须进行的第一个调用。前两个参数对该函数最重要。这两个参数告诉驱动程序您感兴趣的信息类型,并提供通知应用程序信息可用的方式。第一个参数用于位置信息:它是使用 `CreateEvent` 函数创建的 Event 对象的句柄。每当有新的 GPS 位置信息可用时,驱动程序就会设置该事件。第二个参数也是一个 Windows 事件句柄。驱动程序使用该事件来发出任何设备状态更改的信号。
`GPSGetPosition` 和 `GPSGetDeviceState` 函数用于信息检索。每当一个事件被设置时,请使用相应的函数来获取新信息。
当您完成与驱动程序的工作后,通过调用 `GPSCloseDevice` 来关闭它。
代码
很简单,对吧?好的,让我们开始实际实现。在我的演示应用程序中,我有一个 GPS API 的封装类——`CGPSDevice`。该类负责创建事件对象、打开和关闭设备,以及事件监控。事件监控在单独的线程上进行。每当一个事件被设置时,监控线程就会调用包装成自定义接口 `IGPSSink` 的两个方法之一。该接口必须由应用程序实现。在我的例子中,应用程序视图实现了该接口,并在其 `OnPaint` 处理程序中反映任何更改。
#pragma once
//IGPSSink.h
#include <gpsapi.h>
interface IGPSSink
{
virtual HRESULT SetGPSPosition( GPS_POSITION gps_Position ) = 0;
virtual HRESULT SetGPSDeviceInfo( GPS_DEVICE gps_Device ) = 0;
};
#pragma once
//GPSDevice.h
#include <GPSApi.h>
#include "IGPSSink.h"
class CGPSDevice
{
private:
//Singleton instance
static CGPSDevice * s_pInstance;
//Device handle
HANDLE m_hGPS_Device;
//Event for location data updates
HANDLE m_hNewLocationData;
//Event for device state changes
HANDLE m_hDeviceStateChange;
//Thread's handle and id
HANDLE m_hThread;
DWORD m_dwThreadID;
//Exit event
HANDLE m_hExitThread;
//Pointer to sink interface
IGPSSink * m_pSink;
private:
//Our wrapper is singleton make constructor private
CGPSDevice(void);
HRESULT StartThread();
HRESULT StopThread();
static CGPSDevice * Instance();
static DWORD WINAPI GPSThreadProc(__opt LPVOID lpParameter);
public:
~CGPSDevice(void);
static HRESULT TurnOn(IGPSSink * pSink);
static HRESULT TurnOff();
};
//GPSDevice.cpp
#include "StdAfx.h"
#include "GPSDevice.h"
CGPSDevice * CGPSDevice::s_pInstance = NULL;
#define MAX_WAIT 5000
#define MAX_AGE 3000
#define GPS_CONTROLLER_EVENT_COUNT 3
CGPSDevice::CGPSDevice(void)
{
m_pSink = NULL;
m_hGPS_Device = NULL;
m_hNewLocationData = NULL;
m_hDeviceStateChange = NULL;
m_hExitThread = NULL;
}
CGPSDevice::~CGPSDevice(void)
{
}
CGPSDevice * CGPSDevice::Instance()
{
if( CGPSDevice::s_pInstance == NULL )
{
s_pInstance = new CGPSDevice();
}
return CGPSDevice::s_pInstance;
}
DWORD WINAPI CGPSDevice::GPSThreadProc(__opt LPVOID lpParameter)
{
DWORD dwRet = 0;
GPS_POSITION gps_Position = {0};
GPS_DEVICE gps_Device = {0};
CGPSDevice * pDevice = (CGPSDevice*) lpParameter;
HANDLE gpsHandles[GPS_CONTROLLER_EVENT_COUNT] =
{ pDevice->m_hNewLocationData,
pDevice->m_hDeviceStateChange,
pDevice->m_hExitThread
};
gps_Position.dwSize = sizeof(gps_Position);
gps_Position.dwVersion = GPS_VERSION_1;
gps_Device.dwVersion = GPS_VERSION_1;
gps_Device.dwSize = sizeof(gps_Device);
do
{
dwRet = WaitForMultipleObjects(
GPS_CONTROLLER_EVENT_COUNT,
gpsHandles, FALSE, INFINITE);
if (dwRet == WAIT_OBJECT_0)
{
dwRet = GPSGetPosition(
pDevice->m_hGPS_Device,
&gps_Position, MAX_AGE, 0);
if (ERROR_SUCCESS != dwRet)
continue;
else
pDevice->m_pSink->SetGPSPosition(gps_Position);
}
else if (dwRet == WAIT_OBJECT_0 + 1)
{
dwRet = GPSGetDeviceState(&gps_Device);
if (ERROR_SUCCESS != dwRet)
continue;
else
pDevice->m_pSink->SetGPSDeviceInfo(gps_Device);
}
else if (dwRet == WAIT_OBJECT_0 + 2)
break;
else
ASSERT(0);
} while( TRUE );
return 0;
}
HRESULT CGPSDevice::StartThread()
{
HRESULT hr = E_FAIL;
DWORD dwRet = 0;
m_hNewLocationData = CreateEvent(NULL, FALSE, FALSE, NULL);
if ( m_hNewLocationData )
{
m_hDeviceStateChange = CreateEvent(NULL, FALSE, FALSE, NULL);
if (m_hDeviceStateChange)
{
m_hExitThread = CreateEvent(NULL, FALSE, FALSE, NULL);
if (m_hExitThread)
{
m_hThread = ::CreateThread(NULL, NULL,
GPSThreadProc, this, NULL, &m_dwThreadID);
if ( m_hThread )
hr = S_OK;
}
}
}
if( FAILED(hr) )
{
dwRet = GetLastError();
hr = HRESULT_FROM_WIN32(dwRet);
}
if (FAILED(hr))
{
if (m_hNewLocationData)
{
CloseHandle(m_hNewLocationData);
m_hNewLocationData = NULL;
}
if (m_hDeviceStateChange)
{
CloseHandle(m_hDeviceStateChange);
m_hDeviceStateChange = NULL;
}
if (m_hExitThread)
{
CloseHandle(m_hExitThread);
m_hExitThread = NULL;
}
}
return hr;
}
HRESULT CGPSDevice::StopThread()
{
HRESULT hr = E_FAIL;
DWORD dwRet = 0;
if( SetEvent(m_hExitThread) )
{
dwRet = WaitForSingleObject(m_hThread, MAX_WAIT);
if(WAIT_OBJECT_0 == dwRet)
hr = S_OK;
}
if( FAILED(hr) )
{
dwRet = GetLastError();
hr = HRESULT_FROM_WIN32(dwRet);
}
if (m_hNewLocationData)
{
CloseHandle(m_hNewLocationData);
m_hNewLocationData = NULL;
}
if (m_hDeviceStateChange)
{
CloseHandle(m_hDeviceStateChange);
m_hDeviceStateChange = NULL;
}
if (m_hExitThread)
{
CloseHandle(m_hExitThread);
m_hExitThread = NULL;
}
if (m_hThread)
{
CloseHandle(m_hThread);
m_hThread = NULL;
m_dwThreadID = 0;
}
return hr;
}
HRESULT CGPSDevice::TurnOn(IGPSSink * pSink)
{
if( !pSink )
return E_INVALIDARG;
CGPSDevice * pDevice = Instance();
//We already have a device opened
if( pDevice->m_hGPS_Device )
return E_UNEXPECTED;
if( pDevice->m_pSink != NULL )
return E_UNEXPECTED;
pDevice->m_pSink = pSink;
HRESULT hr = pDevice->StartThread();
if( SUCCEEDED(hr) )
{
pDevice->m_hGPS_Device = GPSOpenDevice(
pDevice->m_hNewLocationData,
pDevice->m_hDeviceStateChange,
NULL, NULL);
if( pDevice->m_hGPS_Device )
hr = S_OK;
else
hr = HRESULT_FROM_WIN32(GetLastError());
}
if( FAILED(hr) )
TurnOff();
return hr;
}
HRESULT CGPSDevice::TurnOff()
{
CGPSDevice * pDevice = Instance();
if( !pDevice->m_hGPS_Device )
return E_UNEXPECTED;
HRESULT hr = pDevice->StopThread();
pDevice->m_pSink = NULL;
DWORD dwRet = GPSCloseDevice(pDevice->m_hGPS_Device);
pDevice->m_hGPS_Device = NULL;
if( SUCCEEDED(hr) )
{
if( ERROR_SUCCESS != dwRet )
hr = HRESULT_FROM_WIN32(GetLastError());
}
return hr;
}
如前所述,视图类实现了 `IGPSSink`。
//DemographicsView.h class CDemographicsView : public CWindowImpl<cdemographicsview />, public IGPSSink { ... HRESULT SetGPSPosition(GPS_POSITION gps_Position); HRESULT SetGPSDeviceInfo(GPS_DEVICE gps_Device); ... }
最有趣的方法是 `SetGPSPosition`。这是实际调用 Web 服务的地方。我需要指出的是,为了性能起见,我设置了一个大约 0.5 英里的距离阈值,以限制调用 Web 服务的次数。确保 Web 服务仅在需要时调用是一种好习惯。
//DemographicsView.cpp
HRESULT CDemographicsView::SetGPSPosition(GPS_POSITION gps_Position)
{
::EnterCriticalSection(&m_cs);
m_gpsPosition = gps_Position;
//We will update only if we have data
if( m_gpsPosition.dwSatelliteCount != 0 )
{
double dist = (m_oldLat - gps_Position.dblLatitude) *
(m_oldLat - gps_Position.dblLatitude) +
(m_oldLong - gps_Position.dblLongitude) *
(m_oldLong - gps_Position.dblLongitude);
if( dist > DIST_THRESHOLD )
{
m_oldLat = gps_Position.dblLatitude;
m_oldLong = gps_Position.dblLongitude;
//This is the web service class that
//has been generated by Visual Studio
DemographixQuery::CDemographixQuery webService;
//Invoke the method we are interested in.
HRESULT hr = webService.GetLocationInformationByLatitudeLongitude(
m_gpsPosition.dblLatitude,
m_gpsPosition.dblLongitude,
CComBSTR(DEMO_LICENSE),
&m_info);
}
//Update the view
Invalidate();
}
::LeaveCriticalSection(&m_cs);
return S_OK;
}
HRESULT CDemographicsView::SetGPSDeviceInfo(GPS_DEVICE gps_Device)
{
::EnterCriticalSection(&m_cs);
m_gpsDevice = gps_Device;
Invalidate();
::LeaveCriticalSection(&m_cs);
return S_OK;
}
资源
- Visual Studio 2005
- Windows Mobile 6 Standard 软件开发工具包
- WTL 8.0
- CDYNE 的 Demographics Web Service
- 摩托罗拉 MOTO Q 9h 支持 GPS 的智能手机
历史
- 2008 年 1 月 17 日 - 添加了“目录”部分。
- 2008 年 1 月 16 日 - 初始版本。