通过Windows Mobile连接管理器建立网络连接
EstablishNetworkWithConnMgr.rar
原文为Jim Wilson 的 Establishing Network Connectivity with the Windows Mobile Connection Manager。
概要 Summary
本文主要讲述在托管程序中怎样使用连接管理器建立和释放网络连接。本文的重点是关于使用连接管理器建立和断开连接的概念,而不是如何封装连接管理器API。文章的目的在于涵盖概念使能够适用于任何连接管理器托管API。
适用 Applies to
Windows Mobile 6 Professional
Windows Mobile 6 Standard
Windows Mobile 6 Classic
Windows Mobile 5.0 for Pocket PC Phone Edition
Windows Mobile 5.0 for Smartphone
Windows Mobile 5.0 for Pocket PC
索引
1. Introduction
2. Accessing Connection Manager from Managed Code
3. Establishing a Connection
4. Conclusion
介绍 Introduction
现代Windows Mobile设备包含了许多的网络连接选项,比如Wi-Fi和大量的cellular radios。而且,众所周知,所有Windows Mobile设备能够通过ActiveSync连接桌面计算机访问网络。
所有这些网络选项提供不同的数据速度,并且,在任何时候,0、1或更多的网络连接可能都是可用的。当一个应用程序需要建立一个网络连接时,Windows Mobile提供了一个公共解决方案 —— 那就是连接管理器,而不必要求应用程序自己遍历所有可用的连接然后选择最合适的连接。
就象名字表达的一样,连接管理器负责管理设备上所有的网络连接。当应用程序需要建立一个网络连接时,只需要简单的告诉连接管理器需要哪种连接(比如Internet),连接管理器自己会识别哪些连接可用、选择最佳连接并按照需要建立连接。连接管理器甚至可以在有相同连接需求的多个应用程序中共享一个连接。应用程序完全从细节中抽象出来并且无需考虑任何细节地使用连接。
连接管理器功能强大并提供了大量不同的连接相关的性能。本文主要讲述怎样使用连接管理器建立一个网络连接并且在不需要的时候释放掉。
虽然本文的例子直接p/invoke连接管理Native API,但是本文主要讲述关于使用连接管理建立和断开连接的概念,而不是如何封装连接管理器API。文章的目的在于涵盖概念使能够适用于任何连接管理器托管API。
通过托管代码访问连接管理器 Accessing Connection Manager from Managed Code
连接管理器是Windows Mobile的基础部分,但是当前只以Native API的形式暴露。好消息是大多数API很简单并且能够通过.NET Compact Framework简单地访问。你需要定义一些枚举和结构,但大多数不必去定义,过程非常简单。
【李森 - listen附:Connection Manager wrapper 提及了:他本人向Jim Wilson 通过Email问及了Microsoft Visual Studio 2008下.NET Compact Framework 3.5 连接管理器的封装,Jim Wilson回答说微软放弃了添加连接管理器的封装,而Jim Wilson本人也多次要求微软方面删去本段,但是一直没有修改】
函数Connection Manager Functions
Connection Manager API由11个函数组成,但是只需要使用其中的5个就能完成建立和释放网络连接的工作。在许多情况下,你的程序可能只要其中的2个函数就可以了。表1显示了这5个函数
|
|
|
|
|
|
|
|
|
|
|
取回指定连接的状态。 |
表1 Connection Manager functions used to establish and release a network connection
要从 .NET Compact Framework程序中访问这5个函数,需要P/Invoke,如下:
- [DllImport("CellCore.dll")]
- static extern int ConnMgrMapURL(string url, ref Guid networkGuid, int passZero);
- [DllImport("CellCore.dll")]
- static extern int ConnMgrEstablishConnection(ConnMgrConnectionInfo connectionInfo, ref IntPtr connectionHandle);
- [DllImport("CellCore.dll")]
- static extern int ConnMgrEstablishConnectionSync(ConnMgrConnectionInfo connectionInfo, ref IntPtr connectionHandle, uint dwTimeout, ref ConnMgrStatus dwStatus);
- [DllImport("CellCore.dll")]
- static extern int ConnMgrReleaseConnection(IntPtr connectionHandle, int cache);
- [DllImport("CellCore.dll")]
- static extern int ConnMgrConnectionStatus(IntPtr connectionHandle, ref ConnMgrStatus status);
枚举和结构 Connection Manager Enumerations and Structures
当使用连接管理器建立连接时,必须指定连接的特性。本地代码中需要使用CONNMGR_CONNECTIONINFO和几组 #define 宏来指定连接的特性。为了使结构和.NET的特性相容,可以用托管的类来实现结构并命名为,比如ConnMgrConnectionInfo。与其象.NET一样使用常量来定义宏,不如使用枚举来显式地定义每组相关的宏。
有几个结构依赖于宏,因此首先要定义宏对应的枚举。
第一个枚举对应CONNMGR_PARAM_* 宏集。这些值标识了结构中可用的标准字段,这些字段指定了请求连接中想要的特性。一个连接请求可以指定多个条件,因此,枚举声明应该包含Flags属性。通过表示Flags属性,可以对成员进行位或运算。
- [Flags]
- public enum ConnMgrParam : int
- {
- GuidDestNet = 0x1,
- MaxCost = 0x2,
- MinRcvBw = 0x4,
- MaxConnLatency = 0x8
- }
每个枚举变量就像结构中对应的字段一样拥有相同的名字。下面对于结构定义的讨论将满足每个字段的意义。
下一个枚举对应CONNMGR_FLAG_PROXY_*宏。这些值指定代理服务器的类型,连接管理器使用该类型建立连接。就象ConnMgrParam枚举一样,该声明包含Flags属性。
- [Flags]
- public enum ConnMgrProxy : int
- {
- NoProxy = 0x0,
- Http = 0x1,
- Wap = 0x2,
- Socks4 = 0x4,
- Socks5 = 0x8
- }
下面需要考虑的就是连接请求的优先级。连接管理器负责设备上的所有连接,并尝试服务尽量多连接请求。为了辅助连接管理器决定每个请求的顺序和重要性,每个请求必须指定优先级。连接管理器支持几个不同优先级水平,尽管如此,程序一般只使用几个值。下面的枚举声明显示了通常使用的值:
- public enum ConnMgrPriority
- {
- UserInteractive = 0x8000,
- HighPriorityBackground = 0x0200,
- LowPriorityBackground = 0x0008
- }
表2提供了每个优先级的描述:
优先级 |
|
UserInteractive |
|
HighPriorityBackground |
该请求为高优先级,但是应用程序在后台运行,因此并不影响用户接口。 |
|
|
表2 Common Connection priorities
连接管理器支持了更多的枚举声明,connmgr.h中的CONNMGR_PRIORITY_*宏提供了完整的列表。 在表2中没有提及的优先级值,参考Connection Manager Priority Constants
最后一个枚举是连接状态值。大多数状态值名称无需说明便能理解。对于每个状态值的描述,参见Connection Manager Status Constants或者参见connmgr.h中的CONNMGR_STATUS_*宏。如下:
- public enum ConnMgrStatus
- {
- Unknown = 0x00,
- Connected = 0x10,
- Suspended = 0x11,
- Disconnected = 0x20,
- ConnectionFailed = 0x21,
- ConnectionCanceled = 0x22,
- ConnectionDisabled = 0x23,
- NoPathToDestination = 0x24,
- WaitingForPath = 0x25,
- WaitingForPhone = 0x26,
- PhoneOff = 0x27,
- ExclusiveConflict = 0x28,
- NoResources = 0x29,
- ConnectionLinkFailed = 0x2a,
- AuthenticationFailed = 0x2b,
- NoPathWithProperty = 0x2c,
- WaitingConnection = 0x40,
- WaitingForResource = 0x41,
- WaitingForNetwork = 0x42,
- WaitingDisconnection = 0x80,
- WaitingConnectionAbort = 0x81
- }
所有的枚举定义后,下面要做的就是理解连接请求结构体。可以使用结构或管理类来组织Native结构,一般,类比结构体更流畅,因为类允许提供默认构造函数和成员初始化。上面已经提过了,ConnMgrConnectionInfo管理类代表CONNMGR_CONNECTIONINFO,如下:
- [StructLayout(LayoutKind.Sequential)]
- class ConnMgrConnectionInfo
- {
- Int32 cbSize; // DWORD
- public ConnMgrParam dwParams = 0; // DWORD
- public ConnMgrProxy dwFlags = 0; // DWORD
- public ConnMgrPriority dwPriority = 0; // DWORD
- public Int32 bExclusive = 0; // BOOL
- public Int32 bDisabled = 0; // BOOL
- public Guid guidDestNet = Guid.Empty; // GUID
- public IntPtr hWnd = IntPtr.Zero; // HWND
- public UInt32 uMsg = 0; // UINT
- public Int32 lParam = 0; // LPARAM
- public UInt32 ulMaxCost = 0; // ULONG
- public UInt32 ulMinRcvBw = 0; // ULONG
- public UInt32 ulMaxConnLatency = 0; // ULONG
- } ;
表3描述了各个字段的意义
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
为了简化ConnMgrConnectionInfo类的使用,该类定义了一组构造函数负责一些通用情况。第一个是默认构造函数,如下:
- public ConnMgrConnectionInfo()
- {
- cbSize = Marshal.SizeOf(typeof(ConnMgrConnectionInfo));
- }
在给Connection Manager functions传递ConnMgrConnectionInfo类参数时,需要指定ConnMgrConnectionInfo类的大小,因此构造函数自动存储cbSize作为类的大小。
虽然连接管理器类有许多成员,应用程序通常只需要设置目标GUID、优先级,如果需要,还要设置代理。如果包含目标GUDI,那么dwParams字段必须要设置ConnMgrParam.GuidDestNet值。如下:
- public ConnMgrConnectionInfo(Guid destination, ConnMgrPriority priority, ConnMgrProxy proxy)
- : this()
- {
- guidDestNet = destination;
- dwParams = ConnMgrParam.GuidDestNet;
- dwPriority = priority;
- dwFlags = proxy;
- }
方便起见,该类包含了2个其他的构造函数。第一个接受目标GUID和优先级,不需要代理。第二个参数接受目标GUID,并设置优先级为ConnMgrPriority.UserInteractive,该优先级由程序用户接口等待。
- public ConnMgrConnectionInfo(Guid destination, ConnMgrPriority priority)
- : this(destination, priority, ConnMgrProxy.NoProxy) { }
- public ConnMgrConnectionInfo(Guid destination)
- : this(destination, ConnMgrPriority.UserInteractive) { }
建立连接 Establishing a Connection
使用Native方法,枚举和结构就可以通过连接管理器建立一个连接了。好消息是困难的工作已经做完了,使用连接管理器是非常容易的事情。建立实际的连接包括两步:确定网络目标标识和发送连接请求。
确定目标网络标识 Determining a Network Destination Identifier
建立连接的第一步是标识你的程序是连接到Internet 还是单位网络(Work network)。每个网络目标有一个指定的GUID.Internet的GUID是436EF144-B4FB-4863-A041-8F905A62C572,单位网络(Work network)的GUID是A1182988-0D73-439e-87AD-2A5B369F808B。
虽然我们都知道每个网络目标的GUID,但大多数程序并不直接使用这些GUID.相反,能够功过ConnMgrMapURL函数确定合适的网络GUID.
连接管理器压缩了逻辑来确定url请求哪个网络。因此,与其硬编码一个网络GUID,不如简单的传递一个目标url给ConnMgrMapURL函数来返回一个合适的GUID。
下面的代码演示了使用ConnMgrMapURL去确定不同URL的目标GUID。
- string url1 = "http://msdn2.microsoft.com/en-us/netframework/bb495180.aspx";
- string url2 = "http://hedgydev02/test/default.aspx";
- Guid destination1 = Guid.Empty;
- Guid destination2 = Guid.Empty;
- ConnMgrMapURL(url1, ref destination1, 0);
- ConnMgrMapURL(url2, ref destination2, 0);
- // Write to debug window
- Debug.WriteLine(url1);
- Debug.WriteLine(destination1.ToString());
- Debug.WriteLine("");
- Debug.WriteLine(url2);
- Debug.WriteLine(destination2.ToString());
在上面例子中,有两个URL,第一个为Microsoft's MSDN网址,显然是在Internet上。另一个URL则指向内部网络上的某个电脑,因此是在单位网络(Work network)上。图一显示在运行上述代码后,Visual Studio输出窗口上显示的信息。就像看到的那样,Microsoft URL 返回Internet GUID (436EF144-B4FB-4863-A041-8F905A62C572),而指向内部网的URL则返回Work GUID(A1182988-0D73-439e-87AD-2A5B369F808B)。
图一 The destination network identifiers for an Internet URL and a Work URL
多亏ConnMgrMapURL函数,这也许是你最后一次见到这些GUID了。以后,你只需让ConnMgrMapURL函数为你生成GUID即可。
建立连接 Making the Connection
在这里,只需使用合适的连接标准并且调用合适的连接管理器函数,你就可以创建一个ConnMgrConnectionInfo类实例了。
当请求连接时,你只需调用ConnMgrEstablishConnectionSync函数,该函数将保持阻塞直到连接过程完成;或者使用ConnMgrEstablishConnection函数,该函数立即返回,调用线程无需等待连接过程完成。如果你的应用程序在后台运行,或者你在后台建立连接,可以使用ConnMgrEstablishConnectionSync函数,因为当函数返回时你就能够知道是否成功地建立了连接。下面的代码演示了如何使用ConnMgrEstablishConnectionSync函数建立一个连接:
- IntPtr _connectionHandle = IntPtr.Zero; // Class-level field
- const int _syncConnectTimeout = 60000; // 60 seconds
- void DoConnect(string url)
- {
- Guid networkGuid = Guid.Empty;
- ConnMgrStatus status = ConnMgrStatus.Unknown;
- ConnMgrMapURL(url, ref networkGuid, 0);
- ConnMgrConnectionInfo info = new ConnMgrConnectionInfo(networkGuid, ConnMgrPriority.HighPriorityBackground);
- ConnMgrEstablishConnectionSync(info, ref _connectionHandle, _syncConnectTimeOut, ref status);
- if (status == ConnMgrStatus.Connected)
- Debug.WriteLine("Connect Succeeded");
- else
- Debug.WriteLine("Connect failed: " + status.ToString());
- }
注意,前面的代码给ConnMgrEstablishConnectionSync函数传递了一个延时值。你应该提供一个足够大的延时值,因为,在建立连接时,尤其在一个很差的网络时,有时候要花上几十秒时间。如果函数因为时间到了而返回,相比于连接错误,此时的状态值为ConnMgrStatus.WaitingConnection。当你取回ConnMgrStatus.WaitingConnection状态值时,那么如果你提供了足够的延时值,连接就有可能成功。因此,你应该在尝试建立连接时给与足够的延时时间。
为了测试前面的连接模式,我们使用Windows Mobile 6 Professional Device Emulator 和 Cellular Emulator。Cellular Emulator模拟cellular radio,给Device Emulator提供GPRS连接。开始前,你必须连接Device Emulator到Cellular Emulator,具体参照 Cellular Emulator section 里的Developer's Guide to the Arm 。
你需要为模拟器定义GRPS连接,这和使用真实设备连接到服务网络的步骤一样。按照下面的步骤定义GPRS连接:
1. 在设备模拟器中 “Start - setting”
2. 在“setting”页面选择“Connections” 页面
3. 点击“Connections”图标
4. 然后,选择上面“My ISP”下的“Add a new modem connection”
5. 将连接命名为GPRS Internet。 该名字对连接行为并不起任何作用。
6. 从“Select a modem”下拉列表中选择“Cellular (GPRS) ”
7. 选择“Next”
8. 保持“Access point name”为空,选择Next
9. 保持本页面所有字段为空(User name, Password, Domain),选择“Finish”
现在就定义好了Internet GPRS连接。同样,定义一个单位网络(Work network)GPRS连接,和上面的一样,只需在第4步选择“My Work Network”下“Add a new modem connection”,接着在第5步将连接命名为“GPRS Work”即可。
运行之前的代码后,你就会在几秒后看到Cellular Emulator上“Data Channels”下面的列表有一行上显示有连接活动了。
图二 The Cellular Emulator with an active GPRS connection
Cellular Emulator显示连接以后,ConnMgrEstablishConnectionSync函数就会立即返回了。你应该每次都检查返回状态以确定连接是否真的成功。一个成功的连接返回ConnMgrStatus.Connected状态。
因为ConnMgrEstablishConnectionSync函数在连接进程完全完成以前一直在阻断进程,所以该连接并不适合在面向用户的程序主线程运行,否则将造成程序界面假死现象。为了避免这种情况,就要使用ConnMgrEstablishConnection函数。ConnMgrEstablishConnection 函数并不等待连接完成就直接返回,它只是初始化连接。调用ConnMgrEstablishConnection函数和调用ConnMgrEstablishConnectionSync函数的操作非常相似,如下:
- Guid networkGuid = Guid.Empty;
- ConnMgrMapURL(url, ref networkGuid, 0);
- ConnMgrConnectionInfo info = new ConnMgrConnectionInfo(networkGuid);
- ConnMgrEstablishConnection(info, ref _connectionHandle);
如上代码,ConnMgrEstablishConnection函数只是初始化连接然后在连接管理器真正建立连接前就直接返回了。为了检测连接是否真的完成,应该使用SystemState类和ConnMgrConnectionStatus函数协同使用来监测。
下面的代码显示了当连接数发生改变时,SystemState类如何通知应用程序:
- SystemState _connectionsCount;
- private void Form1_Load(object sender, EventArgs e)
- {
- _connectionsCount = new SystemState(SystemProperty.ConnectionsCount);
- _connectionsCount.Changed += new ChangeEventHandler(ConnectionsCount_Changed);
- }
这段代码构造了_connectionsCount实例,并把它和ConnectionsCount属性关联起来,将ConnectionsCount_Changed方法和_connectionsCount.Changed事件关联起来。只要连接数量发生改变时,_connectionsCount实例就调用ConnectionsCount_Changed函数。
一旦你知道活动连接的数量发生改变,你需要检测发生的改变是否是由于你请求的连接造成的。下面的代码描述了如何使用ConnMgrConnectionStatus函数来决定连接数量的改变是否是由你之前通过ConnMgrEstablishConnection 函数建立的连接引起的。
- void ConnectionsCount_Changed(object sender, ChangeEventArgs args)
- {
- ConnMgrStatus status = ConnMgrStatus.Unknown;
- ConnMgrConnectionStatus(_connectionHandle, ref status);
- if (status == ConnMgrStatus.Connected)
- {
- // Connection Established
- }
- }
通过上面的代码,当连接状态为ConnMgrStatus.Connected时,你就知道你的连接已成准备好并可以使用了。必须要记住,系统在主线程里调用ConnectionsCount_Changed方法,因此,你不能在该方法中执行任何长时间运行的任务。相反,你应该在后台线程执行任务或者使用异步方法,就像SqlCeReplication类中的BeginSynchronize方法一样。
你可以使用Device Emulator和Cellular Emulator测试你的代码。
释放连接 Releasing the Connection
释放连接的操作非常简单。大多数情况下,你只需要简单的给ConnMgrReleaseConnection函数传递之前从ConnMgrEstablishConnectionSync或ConnMgrEstablishConnection 函数返回的连接句柄即可。ConnMgrReleaseConnection函数给连接管理器发送信号告诉它程序已经使用完连接,但是该函数并没有真正的关闭它。要理解什么时候连接才会真正的关闭,你需要理解ConnMgrReleaseConnection的lCache参数以及连接管理器是如何管理连接缓存的。
缓存连接 Caching the Connection
正如我们所知道的,建立连接会很消耗时间,为了避免在重复建立相同的连接上的花费,有时候在最后一个程序释放完连接以后,连接管理器还会继续维持该连接。这就是连接缓冲机制。应用程序能通过设置ConnMgrReleaseConnection 函数的lCache参数来改变连接管理器缓冲一个连接的时间,但最终做决定的还是连接管理器。表4显示了lCache参数可能的值:
|
|
|
|
|
|
|
表4 ConnMgrReleaseConnection cache parameter values
如表4所示,如果参数设置为0,那么连接管理器立即关闭连接。设置为1则缓冲默认的时间,如下:
- // Release the connection and cache it for the default time period.
- ConnMgrReleaseConnection(_connectionHandle, 1);
默认的缓冲时间定义在[HKEY_LOCAL_MACHINE\Comm\ConnMgr\Planner\Settings]注册表下的某个键值中。默认缓冲时间依赖于连接是否以独占模式创建。CacheTime键的值定义了非独占模式的默认缓冲时间,在大多数设备中被设置为600秒(10分钟)。对于独占模式,VPNCacheTime键定义了缓冲时间,大多数设备中位60秒(1分钟)。
Windows Mobile 5.0以前,程序只能指定连接是否能够缓存。Windows Mobile 5.0和Windows Mobile 6以后,你能够指定连接管理器缓存连接的时间。而这些只需要简单给ConnMgrReleaseConnection函数的lCache参数设置想要的时间数,而不是设置为0或1。如下代码,设置缓冲时间为300秒(5分钟)。
- // Release the connection and cache it for 5 minutes.
- ConnMgrReleaseConnection(_connectionHandle, 300);
应该时刻记住,应用程序给ConnMgrReleaseConnection函数的lCache参数传递值只是一个请求,而连接管理可以选择忽略该请求。大多数情况下连接管理器不会考虑你的缓冲请求,连接管理器会将连接保持更长的缓冲时间。比如,当通过Ethernet cable连接网络或设备通过ActiveSync连接到桌面网路,那么设备将会有一个持续的连接。象这些类型的连接,网络连接是由Ethernet cable或依托设备(cradled device)建立的,他们会一直保持连接直到物理设备断开了连接。连接管理器管理这些连接,但是实际上却不打开或关闭它们。
另一种情况,连接管理器会在程序发送断开连接请求时保持更长的缓冲时间,那就是在维持该连接并不需要任何花费时。最普通的例子就是GPRS连接,GPRS radios并不消耗电源除非在进行数据传输时才会。而且,大多数情况下,移动运营商是根据连接的流量而不是连接时间来对GPRS收费的。
你可以通过Cellular Emulator看到连接管理器缓冲GPRS连接的过程,此时要确保Device Emulator核Cellular Emulator时刻保持连接。如果Cellular Emulator仍然保持活动的连接,可以通过点击Network页面下Disconnect GPRS按钮来断开连接。现在,运行以下代码:
- Guid networkGuid = Guid.Empty;
- ConnMgrStatus status = ConnMgrStatus.Unknown;
- ConnMgrMapURL(url, ref networkGuid, 0);
- ConnMgrConnectionInfo info = new ConnMgrConnectionInfo(networkGuid, ConnMgrPriority.HighPriorityBackground);
- Debug.WriteLine("Attempting Sync Connect");
- ConnMgrEstablishConnectionSync(info, ref _connectionHandle, _syncConnectTimeOut, ref status);
- if (status == ConnMgrStatus.Connected)
- ConnMgrReleaseConnection(_connectionHandle, 0);
- else
- Debug.WriteLine("Connection failed: " + status.ToString());
代码先调用过ConnMgrEstablishConnectionSync 函数,然后紧接着调用ConnMgrReleaseConnection函数。注意,我们给ConnMgrReleaseConnection函数的Cache参数传递0,来请求连接管理器立即关闭连接。
运行该代码后,你会在Cellular Emulator发现,之前的连接和以前一样还是处于活动状态,你会发现连接根本就没终止。Time(s)列的时间值一直保持增长,连接维持活动状态但是却不传输任何数据,而且,在真实设备中这并不会消耗任何电源。连接管理器忽略了断开请求中的不缓冲请求,而是继续连接而不消耗任何有效资源。
这种连接类型有时被称作“suspend/resume”(挂起/恢复)连接,也就是,当不再使用连接时,连接仍然被保持,只是进入高效的挂起状态。GPRS连接一般会保持无限持续连接状态,直到连接管理器返回该连接给应用程序并使用它时才活动。
结论 Conclusion
连接管理器是Windows Mobile中最强大的因素之一,它压缩了管理连接的细节,自动选择最好的连接,甚至跨程序共享连接。它支持大量特性,有时这会吓到用户,但是,大多数程序不必关心连接管理器的扩展因素。只需使用其中几个函数就可以轻松的建立连接,而让连接管理器去处理内部的细节。
其他资源 See Also
Windows Mobile Cellular Emulator
Windows Mobile Device Emulator
翻译完了,有点累,呵呵! 以上译文,如有错误,欢迎指出!
我将文章所有的代码添加到了下面的工程中,上面的文章在获取连接状态时使用的是SystemState,我在代码里做了添加。另外,我还添加了使用MessageWindow派生类来获取连接状态的方法,并将其窗口句柄传递给ConnMgrConnectionInfo类的hWnd字段。当然,如上所说,你也可以将窗口句柄传递给ConnMgrRegisterForStatusChangeNotification函数的第二个参数,也能达到同样的效果!
代码下载:
EstablishNetworkWithConnMgr.rar
posted on 2009-12-23 14:28 listenlisten 阅读(4056) 评论(22) 编辑 收藏 举报