2008年7月26日星期六

Windows Mobile枚举网络链接

本文适用于Windows Mobile 2003以上版本。

有很多Window Mobile(一下简称WM)的程序需要访问网络,但是在国内环境下,访问网络还是比较昂贵的。因此大多数程序都是在需要的时候自动建立网络连接,然后访问网络,做一些事情之后断开链接。所要求的也就是用户事先设定好网络连接的帐号。

要完成这样的功能,需要利用Windows Mobile SDK中的Connection Manager。只需要三个函数即可完成任务。

  • ConnMgrEnumDestinations
  • ConnMgrEstablishConnectionSync/ConnMgrEstablishConnection
  • ConnMgrReleaseConnection
需要的包含的头文件为<connmgr.h>

其实想法很简单,我们利用ConnMgrEnumDestinations枚举可用的网络,然后利用ConnMgrEstablishConnectionSync/ConnMgrEstablishConnection(前者在建立链接完毕之前不会返回,后者直接返回,本文使用前者为例),最后使用ConnMgrReleaseConnection断开链接。

示例代码如下:


HRESULT hResult = E_FAIL;
CONNMGR_DESTINATION_INFO DestInfo;
int index = 0;
hResult = ConnMgrEnumDestinations(index++, &DestInfo);
TCHAR* str1 = _T("Internet");
TCHAR* str2 = _T("单位");
while(SUCCEEDED(hResult))
{

if( _tcsstr(DestInfo.szDescription, str1) != NULL ||
_tcsstr(DestInfo.szDescription, str2) != NULL )
{
//
// try to connect
CONNMGR_CONNECTIONINFO ConnInfo;
ZeroMemory(&ConnInfo, sizeof(ConnInfo));
ConnInfo.cbSize = sizeof(ConnInfo);
ConnInfo.dwParams = CONNMGR_PARAM_GUIDDESTNET;
ConnInfo.dwPriority = CONNMGR_PRIORITY_USERBACKGROUND;
ConnInfo.bExclusive = TRUE;
ConnInfo.guidDestNet = DestInfo.guid;
HANDLE hConnection;
DWORD status;
hResult = ConnMgrEstablishConnectionSync
(&ConnInfo, &hConnection, 60*1000,&status);
if( SUCCEEDED(hResult) &&(status & CONNMGR_STATUS_CONNECTED)){
hResult = ConnMgrReleaseConnection(hConnection, FALSE);
break;
}
else{
switch(status)
{
// do something;
}
}
}
hResult = ConnMgrEnumDestinations(index++, &DestInfo);
}



程序相当简单,因为一般WM的手机都使用Internet(CMNET)或者单位链接(CMWAP)来连入网络,所以我把其他的网络过滤掉了。

注意红色标出的部分,这里表明该链接是独占的。只有这样的链接才能断开。

2008年7月8日星期二

windows绘图概述

这两天在开发的时候遇到好多绘图的问题,发现自己在绘图这块实在又是门外汉一个。于是翻出MSDN仔细研究了一下,搞懂了很多概念。我觉得这对于windows编程来说是非常重要的概念,因此仔细的把MSDN上相关的信息全部读了一遍,同时做了翻译和笔记。我贴在下面,希望对跟我一样的朋友有所帮助。
另有PDF版本可以从我的namipan下载->WindowsPaintingAndDrawing.pdf

Windows 绘图详解


几乎所有的windows程序都会在屏幕上绘图,但是由于多任务和多窗口,为了使绘制图形平滑,漂亮,应用程序使用windows作为其主要输出设备,而不是屏幕。

系统提供了与窗口一致的Device context。应用程序就使用DC把输出定向到目标窗口。


什么时候在窗口中绘图?

一个应用程序在很多时候都会在窗口中绘图。比如,第一次创建窗口的时候,当改变了窗口大小的时候,当把一个窗口从别的窗口后面拉出来的时候,当最大最小化窗口的时候,当打开文件显示内容的时候,当滚动,改变或者选择了一部分数据进行显示的时候等等。


系统会管理类似移动窗口和改变大小的操作,如果一个操作影响了窗口的内容(如果屏幕上只有一个窗口,把这个窗口在屏幕范围内不断拖动,不会引发重绘),系统会把这部分被影响了的部分标记出来,以备后面绘图时使用,然后,发送WM_PAINT消息给窗口函数。这个消息告诉了应用程序哪些地方必须被重绘,使得应用程序能够实现有效的重绘。


同样的,应用程序也可以标记需要重绘的区域,一旦标记,也会导致发送WM_PAINT消息。如果一个操作需要立刻得到回应,应用程序可以在操作的同时进行绘图,这样就不必等到下一次WM_PAINT事实上WM_PAINT产生的条件就是有部分窗口失效了。如果不出现这种情况,就不会产生WM_PAINT


在任何情况下,只要窗口创建了,应用程序就可以在里面绘图。为了能够绘图,应用程序首先应该获得一个设备描述表的句柄。在理想情况下,应用程序的大多数绘图在WM_PAINT中完成。在处理该消息的时候,通过调用BeginPaint就能获得DC的句柄。如果应用程序在其他时候进行绘图,可以通过GetDC或者GetDCEx来得到DC的句柄。


WM_PAINT 消息

当窗口的Client Area发生改变的时候,系统给应用程序发送WM_PAINT消息。系统仅在消息队列中没有其他消息的时候发送该消息给应用程序。一般的用法是在WM_PAINT中调用BeginPaint获得DC,然后进行任何GDI绘图,最后通过EndPaint释放DC


beginPaint返回之前,系统为指定的窗口准备DC。它会为DC设置裁剪区域,这块区域就是需要重绘的区域。任何在此区域之外的绘制都会被裁减掉。


beginPaint结束之前,系统还会发送WM_NCPAINTWM_ERASEBKGND两个消息给应用程序。这些消息告诉应用程序绘制Non-Client Area和窗口背景。Nonclient Area也是window的一部分,其包括title barsystem menuscrollbar。大多数应用程序依赖DefWindowProc来绘制这些区域,因此一般会把WM_NCPAINT发送给DefWindowProcWindow的背景是用颜色或者笔刷在任何绘制操作之前填充出来的。背景会盖住任何之前存在的图片或者窗口后面的屏幕。如果一个windowwindow class定义了背景笔刷,DefWindowProc用这个笔刷自动绘制背景。


BeginPaint会填充一个PAINTSTRUCT结构。该结构中包含需要绘制的区域的大小等信息。应用程序可以利用这个信息,(存放在rcpaint中)来确定需要绘制的地方。如果需要输出的内容很简单,也可以不管这个信息,直接输出,让window自己来裁剪。


BeginPaint会把需要更新的区域设为NULL,以防连续导致发送WM_PAINT消息。即没有需要更新的区域就不会发送该消息。如果一个应用程序在WM_PAINT中没有调用BeginPaint或者清除更新区域,则系统会不断发送WM_PAINT,直到更新区域为空为止。在任何情况下,应用程序都必须在离开WM_PAINT之前清空更新区域。


绘制结束,应该调用EndPaintEndPaint释放DC,使得其他window可以使用。如果之前caretBeginPaint隐藏,EndPaint会显示出来。


更新区域(The Update Region

更新区域是window的一部分,这一部分已经过期或者无效了,需要重绘。系统依靠更新区域向应用程序发送WM_PAINT消息。系统只会把无效的区域加到更新区域中去。


当系统发现有一部分需要重绘的时候,它把这部分加入到更新区域中,但不会立刻要求重绘。只有等到系统把消息队列中的消息处理完之后,系统才会检查更新区域,如果更新区域不是空的,则发送WM_PAINT消息。


应用程序可以自己设置更新区域。比如读取一个文件,然后设置更新区域,这样在之后的WM_PAINT中就可以显示文件内容了。但一般来说,应用程序不应该在数据改变的时候绘制,而应该把所有的绘制工作放到WM_PAINT中去。


更新区域的有效化和无效化( Invalidating and Validating Update Region)

应用程序可以通过调用InvalidateRectInvalidateRgn来无效化一块区域,并设置更新区域。这两个函数把指定的矩形和区域加入到更新区域中。与之前加入进去的区域结合起来。


这两个函数不会导致WM_PAINT消息。Window在处理其他消息的时候,系统不断积累无效区域,直到消息队列中不再有任何消息。


ValidateRectValidateRgn两个函数的作用则相反,用来有效化一块区域,并把这块区域从Update Region中去掉。


获得更新区域

使用GetUpdateRectGetUpdateRgn两个函数来获得当前的更新区域。前者获得最小的能全部包含更新区域的矩形,后者返回更新区域。这两个函数用来获得当前的更新区域来精确定位输出。


BeginPaint也会获得包含全部更新区域的最小矩形。该矩形就在PAINTSTRUCT中的rcPaint。因为BeginPaint是更新区域全部有效话,因此在这之后调用GetUpdateRectGetUpdateRgn返回的都是空区域。


同步和异步绘图

大多数利用WM_PAINT来实现的绘图都是异步的。即在window的部分失效之后有一个短暂的等待,才能进行绘图。在这个短暂的等待中,system从消息队列中获取其他消息,并进行处理。因为system认为WM_PAINT是个低优先级的消息。


但有时需要同步绘图,也就是在窗口的部分失效之后立刻就绘图。比如在创建主窗口之后立刻把它绘制出来等等。基本上来说,那些需要立刻绘图的都是需要和用户交互的部分,同步绘图可以保证不影响交互的性能。


UpdateWindowRedrawWindow两个用来同步绘图。如果更新区域非空,UpdateWindow能够立刻发送一个WM_PAINT消息给应用程序。RedrawWindow同样发送WM_PAINT消息给window,但是它给绘制更多的自由,比如是否要绘制nonclient区域、背景,以及是否不管更新区域非空也能发送WM_PAINT消息等等。这两个函数无论消息队列中有多少其他消息,他们都能直接发送WM_PAINTwindow


任何会消耗一定时间的绘图都应该使用异步绘图,免得程序被block住。一个程序如果经常是一小部分window无效化,最好把这些部分合起来,在一次WM_PAINT中进行处理。


不通过WM_PAINT来绘图

尽管大多数时候应用程序在WM_PAINT处理绘图,但是有时候不通过WM_PAINT而直接进行绘图可能效率更高。这一般都是用于需要立刻得到反馈的情况下,比如用户选择了一部文字,拖曳或放大缩小一个对象时。这些情况下,应用程序通常在键盘和鼠标操作中进行绘图。


不在WM_PAINT中绘图时,应用程序通过GetDCGetDCEx两个函数获得窗口的设备描述表(记住,不是client区域,而是整个窗口)。结束绘制使用ReleaseDC释放DC


当不使用WM_PAINT的时候,应用程序使用一种技巧来进行“可恢复”的绘制。比如在反选一些文字的时候,仅仅只要对那一块区域反色即可,当取消反选的时候,只要再次反色即可。


Window区域

除了更新区域,每一个window都有一个可视区域,即用户能看到的所有区域。当window的大小改变时,或者被其他Windows挡住时,或重新显示时,都要绘制这部分区域。用户没法直接改变这个区域,但是系统自动使用这个区域来表示该windowDC中的裁剪区域



裁剪区域决定了系统的那些部分允许绘图,当应用程序通过BeginPaintGetDCGetDCEx获得DC之后,系统为这些DC都设置了裁剪区域。应用程序可以通过SetWindowRgn, SelectClipPath and SelectClipRgn等函数来设置裁剪区域。


WS_CLIPCHILDRENWS_CLIPSIBLINGS这两个样式告诉系统怎么来计算window的可视区域。如果一个窗口有其中一个样式,窗口就会把子窗口或者兄弟窗口(具有相同Parent的窗口)的可视区域排除在外。


Window背景

Window背景是在进行绘图之前对client区域填充的颜色或者笔刷。背景覆盖了屏幕上窗口区域中的任何东西,擦除了以前的图像,防止之后的绘图颜色混合。


系统可以自己绘制背景,也可以在调用BeginPaint的时候给应用程序发送一个WM_ERASEBKGND消息把绘制的机会让给应用程序。如果由DefWindowProc来处理,则系统使用创建窗口时指定的笔刷进行背景绘制。如果笔刷无效或者window class没有定义背景笔刷,则设置PAINTSTRUCT结构中的fErase为非空,告诉应用程序,由应用程序来负责背景绘制。


如果应用程序处理WM_ERASEBKGND,则需要使用WPARAM来进行绘图。WPARAM包含一个窗口的DC句柄(是的,已经帮你获得了)。在绘制完毕之后,应用程序需要返回一个非空的值。这样BeginPaint就不会把fErase设置为非空值了。(从这里可以看出,WM_ERASEBKGND是通过SendMessage发送的,所以在这个函数中处理要快!)即使window class的背景笔刷已经被定义好了,应用程序还是能够处理WM_ERASEBKGND消息的。



改变window大小

当用户用鼠标拖拉window边框时,通过系统菜单选择最大最小化时或者使用SetWindowPos函数时,系统就会改变window大小。当window改变大小时,系统假设之前显示在外的内容没有被影响到,不需要重绘,系统仅仅使新暴露出来的那部分窗口失效,这样可以在处理WM_PAINT时节省时间。在这种情况下,即使window的大小减小,系统也不产生WM_PAINT

但是有些窗口需要不时的重绘,比如一个钟表应用程序,每次大小改变,都需要重绘。当横向,纵向的任意部分或者两者都发生改变时,为了强制应用程序重绘整个client区域,应用程序必须设置CS_VREDRAWCS_HREDRAW这两个样式。有了这两个样式,任何改变大小的操作都会引起重绘。



Nonclient区域

当任何一部分Nonclient区域,比如title barmenubarwindow frame需要重绘时,系统会给应用程序发送一个WM_NCPAINT息。系统也可以发送其他消息来更新一部分Nonclient区域。例如,当一个窗口被激活或者非激活时,系统发送WM_NCACTIVATE来更新Title Bar。一般来说,对普通window最好不要处理这些消息(带NC的),因为应用程序必须处理所有要求处理的Nonclient区域。

但是如果一个应用程序想要自定义Nonclient区域,则必须处理这些消息。应用程式必须使用窗口的DC来进行绘制。应用程序可以通过GetWindowDCGetDCEx来获得窗口DC。当绘制结束,必须使用ReleaseDC来释放DC

系统也会为Non-clinet区域维护一个更新区域。当一个应用程序收到WM_NCPAINT消息的时候,wParam指向一个包含一个指定了更新区域的Rgn handle。应用程序可以使用这个句柄使更新区域和DC的裁剪区域组合起来。当获得DC是,window不会自动组合更新区域,除非使用GetDCEx并且同时指定这个更新区域的handleDCX_INTERSECTRGN标志。如果应用程序不组合更新区域,则只有超出window以外的绘制操作会被裁减掉。无论是否使用该更新区域,应用程序不需要负责清空更新区域。

如果应用程序要处理WM_NCACTIVATE消息,在处理完毕之后需要返回TRUE来告诉系统完成被激活窗口的更新工作。当应用程式收到WM_NCACTIVATE时如果是最小化情况下,应用程序应该把该消息转发给DefWindowProc,在这种情况下,这个默认的函数会重绘Taskbar上面的这个icon

子窗口更新区域

子窗口是具有WS_CHILD或者WS_CHILDWINDOW样式的窗口。和一般窗口一样,子窗口通过WM_PAINT来绘图。子窗口也维护一个更新区域,应用程序和系统都可以通过设置该更新区域来产生WM_PAINT消息。

子窗口的更新和显示区域受到父窗口的影响,其他样式的窗口则不会。系统常常设置父窗口的更新区域的同时设置子窗口的更新区域,使父窗口收到WM_PAINT消息的同时子窗口也能收到WM_PAINT消息。系统把子窗口的位置限制在父窗口的client区域,超出这个区域就会被裁减掉。

无论何时,只要父窗口的更新区域包含了子窗口的一部分,系统就会为子窗口设置更新区域。此时,系统先向父窗口发送WM_PAINT消息,然后向子窗口发送消息让子窗口可以恢复被父窗口覆盖的内容。

但是如果只有子窗口设置了更新区域,系统不会给父窗口也设置。在无效化子窗口时,系统不会给父窗口发WM_PAINT(因为被覆盖住了,根本没有必要)。同样的,如果使被子窗口覆盖住的父窗口的部分无效化,系统也不会给父窗口发送WM_PAINT的。在这种情况下,无论子窗口还是父窗口都不会收到WM_PAINT消息。

应用程序如果设置了WS_CLIPCHILDREN这个样式的话,当父窗口的更新区域被设置的时候,子窗口的更新区域不会被设置。任何在子窗口下面的绘图全部被裁减掉,因此继续给子窗口发送WM_PAINT消息也是没有必要的了。

子窗口的更新和可视区域也受到兄弟窗口的影响。如果两个窗口重叠,则两个窗口都会收到WM_PAINT消息。他们受到WM_PAINT消息的顺序与z-index相反,即最上面的(z-order最高)的收到WM_PAINT消息最晚。

应用程序可以设置WS_CLIPSIBLING来避免兄弟窗口的绘制重叠。设置了这个,高z-order的窗口部分就被下面的窗口裁减掉了。

2008年7月3日星期四

fill_n与generate_n的区别

有些人对未知事物的第一印象一般比之后的印象记得牢,即使是错误的想法,也会根生地固的记得很牢。而有些人却善于用之后正确的印象来代替之前的印象。我很羡慕这种人,因为我自己是第一种人。

我对fill_n这个函数的印象就属于自以为是的错误印象,然而,这个印象被之后的正确印象所替换之后,我还不时的用错这个函数。就像程序员们老生常谈的空指针问题(现在少的多了,铺天盖地的高级语言,很少遇到这种问题了)。

我第一次看到这个函数,我就立马意识到这是初始化容器的好方法。比如

list<int> l1;
fill_n(back_inserter(l1), 200, 1000);

仅仅一行代码,就可以初始化200个元素,每个赋值为1000.多么方便啊。不过这种简单的类型我是没兴趣的。最重要的是可以初始化类,这就是我对其错误认识的开始。为了测试这个程序,我曾写下了如下代码:


//
// test.h

class TObj{
public:
~TObj(){cout<<"Deleting"<<endl;}
};
struct Dest_TObj{
template<class T> T* operator()(T* p){ delete p; return 0; }
};
int main(){
list<TObj*> tl;
fill_n(back_inserter(tl), 20, new TObj());
transform(tl.begin(),tl.end(),tl.begin(), Dest_TObj());
return 0;
}


在main函数中,第二行为tl初始化,20个元素,每个元素是一个指向TObj对象的指针,该指针指向一个新分配的TObj对象。然后在transform中对tl进行purge。如果不用这两个函数,我们只能用循环的方法:

for( int i = 0; i < 20; ++i) tl.push_back(new TObj());
for( list<TObj*>::iterator iter = tl.begin();
iter!= tl.end();++iter)
{
delete (*iter);
}

尽管看起来也行,但是总觉得比较原始,不是吗?

结果,当我满怀信心的运行程序,运行到transform时就崩溃了。我左看右看没发现问题。看看输出,有一个Deleting。那说明至少成功了一次,难道后面指针指错了?我把transform换成了上面的循环,结果也是运行了一次就崩溃了。看来问题在初始化的地方。
我把fill_n替换成上面的循环。程序欢快的跑完了。Shit,还有这事!

看来是fill_n有问题,我只能再回去仔细看fill_n的原型:
template<class>void fill_n(Out res, Size n, const T& val);
我终于注意到了最后一个参数,这是一个值参,fill_n只不过把每一个元素都用这个值进行赋值而已!因此,我希望初始化的20个TObj对象,实际上只有一个,所有的指针都指向这个对象。当第一个被delete之后,后面的就成了野指针。企图删除一块已经被释放了的内存块,当然结果是崩溃了。

如果要实现我需要的想法,只能使用generate_n。该函数和fill_n类似,只不过把最后一个参数换成了一个函数,调用该函数n次。
比如我有一个Create_TObj()的函数,返回一个新创建的TObj对象的话,我就可以把上面的代码改成:
generate_n(back_inserter(tl),20,Create_TObj());
这样程序就能顺利执行了。

不幸的是,这个错误我是屡犯不改,终于不得已写下此文,希望能长点记性。