什么是映射模式呢?为了说清楚这个概念,我们先介绍两个名词:“窗口”、“视口”。

视口是基于设备坐标的。对于显示器,就是像素的,也就是你看到的。而窗口是基于逻辑坐标的,虚拟的,也是你写程序时使用的。而且与你当前拿到的设备描述表有关,一般通过beginPaint拿到的都是客户区;而使用getDC拿到的则是通常意义下的窗口:客户区+菜单栏+工具栏+标题栏等等。

而窗口到视口的坐标映射,就是映射模式。用数学公式表述为:

窗口到视口:

xViewport = (xWindow-xWinOrg)*xViewExt/xWinExt+xViewOrg

yViewport = (yWindow-yWinOrg)*yViewExt/yWinExt+yViewOrg

其中xViewExt表示的是视口的横坐标范围,xWinExt表示的是窗口的横坐标范围,通常我们关心的不是它们的具体值,而是二者的比例。

windows提供了8种映射模式

映射方式

逻辑单位

增加值

x值

y值

MM_TEXT

图素

MM_LOMETRIC

0.1 mm

MM_HIMETRIC

0.01 mm

MM_LOENGLISH

0.01 in.

MM_HIENGLISH

0.001 in.

MM_TWIPS

1/1440 in.

MM_ISOTROPIC

任意(x = y)

可选

可选

MM_ANISOTROPIC

任意(x != y)

可选

可选

这8种模式通常分为3大类:MM_TEXT、“度量”映射方式、“自作主张”的映射方式。下面对他们一一进行介绍:

MM_TEXT
之所以称为MM_TEXT,是因为这个方式在默认情况下与我们看书的方式相同:原点在左上角,从左向右,从上到下。

对于MM_TEXT映射方式,内定的原点和范围如下所示:

窗口原点:(0, 0) 可以改变

视口原点:(0, 0) 可以改变

窗口范围:(1, 1) 不可改变

视口范围:(1, 1) 不可改变

这意味着窗口坐标与逻辑坐标之间没有比例变换,只有原点设置引起的平移变换。所以它也称为称为“全约束”的映射方式。你在窗口位置中+1,则在屏幕上移动一个像素。

你可以通过SetViewportOrgEx和SetWindowOrgEx,用来改变视口和窗口的原点,但一般我们只改变一个就能达到满意的效果,两个同时改变则容易产生混乱。

通过GetViewportOrgEx和GetWindowOrgEx来获取目前视端口和窗口的原点。

举一个例子,假如我们要绘制正弦曲线,可以:(完整的程序见于:windows程序设计(6):基本画图 )

case WM_PAINT:
		hdc = BeginPaint(hwnd,&ps);

		
		//画一条直线
		MoveToEx	(hdc,0,			cyClient/2,NULL);
		LineTo		(hdc,cxClient,	cyClient/2);
		//绘制正弦曲线
		for(int i = 0;  i< NUM;i++)
		{
			//把x轴等分成1000份
			apt[i].x = i * cxClient / NUM;
			apt[i].y = (int) (cyClient / 2 * (1-sin(TWOPI * i /NUM)));
			LineTo(hdc,apt[i].x,apt[i].y);
			//Sleep(10);
			
		}

		

		EndPaint(hwnd,&ps);
		return 0;

我们可以改变视口的原点:

case WM_PAINT:
		hdc = BeginPaint(hwnd,&ps);
		//设置视口原点		
		SetViewportOrgEx(hdc,cxClient/2,cyClient/2,NULL);


		
		//画一条直线
		MoveToEx	(hdc,0,			cyClient/2,NULL);
		LineTo		(hdc,cxClient,	cyClient/2);
		//绘制正弦曲线
		for(int i = 0;  i< NUM;i++)
		{
			//把x轴等分成1000份
			apt[i].x = i * cxClient / NUM;
			apt[i].y = (int) (cyClient / 2 * (1-sin(TWOPI * i /NUM)));
			LineTo(hdc,apt[i].x,apt[i].y);
			//Sleep(10);
			
		}

		

		EndPaint(hwnd,&ps);
		return 0;

可以预见到,将视口的原点设成了客户区的中间,而我在画图时,第一条直线是从原点下面半个客户区的范围画的,所以直线刚好看不到。而正弦曲线也只能看到一半。

通过更改视口的原点,还有一个重要的应用,就是做出字幕“滚动”的效果:

程序的其他部分见于:windows程序设计(6):基本画图

LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	//字符的宽度,大写字母宽度,字符高度
	static int    cxChar, cxCaps, cyChar, iBegin;
	//窗口大小
	static int cxClient, cyClient ;

	//滚动条位置
	static int iVertPos,iHorzPos,iPaintBeg,iPaintEnd;
	HDC hdc;
	//该变量用于索引sysmets.h中定义的结构体数组sysmetrics[]的每个元素
	int i;
	//输出文本的位置
	int x,y;
	//绘图结构
	PAINTSTRUCT ps;

	//字符串
	TCHAR szBuffer [10];
	//字体信息结构
	TEXTMETRIC  tm;
	switch(message)
	{
	case WM_CREATE:
		SetTimer(hwnd,ID_TIMER,20,NULL);
		hdc = GetDC(hwnd);
		//取得内定系统字体的文字大小,存在放在tm里
		GetTextMetrics (hdc, &tm);
		//平均字符宽
		cxChar = tm.tmAveCharWidth ;
		//大写字母的平均宽度
		cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2 ;
		//字符总高度:高度+行间距
		cyChar = tm.tmHeight + tm.tmExternalLeading ;

		ReleaseDC(hwnd,hdc);

		return 0;
	case WM_TIMER:
		iBegin++;
		InvalidateRect(hwnd,NULL,TRUE);

	case WM_SIZE:
		cxClient = LOWORD (lParam) ;        
		cyClient = HIWORD (lParam) ;   
		return 0; 
		
	case WM_PAINT:
		hdc = BeginPaint (hwnd, &ps) ;
		//不断地改变原点
		SetWindowOrgEx(hdc,0,iBegin,NULL);

		for(i = 0; i<NUMLINES;i++)
		{
			TextOut(hdc,0,cyChar*i,sysmetrics[i].szLabel,lstrlen(sysmetrics[i].szLabel));
			TextOut(hdc,22 * cxChar,cyChar*i,sysmetrics[i].szDesc,lstrlen(sysmetrics[i].szDesc));
			SetTextAlign(hdc,TA_RIGHT | TA_TOP);
			TextOut(hdc,22*cxCaps + 40 * cxChar,cyChar * i,szBuffer,wsprintf(szBuffer,TEXT("%5d"),GetSystemMetrics(sysmetrics[i].Index)));
			SetTextAlign(hdc,TA_LEFT | TA_TOP);
		}
		
		EndPaint (hwnd, &ps) ;
		return 0;
		

	case  WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	}
	return DefWindowProc (hwnd, message, wParam, lParam) ;

}

“度量”映射方式

具体包含一下5种:

映像方式

逻辑单位

英寸

毫米

MM_LOENGLISH

0.01 in.

0.01

0.254

MM_LOMETRIC

0.1 mm.

0.00394

0.1

MM_HIENGLISH

0.001 in.

0.001

0.0254

MM_TWIPS

1/1400 in.

0.000694

0.0176

MM_HIMETRIC

0.01 mm.

0.000394

0.01

之所以成为“度量”,因为它们使用的是实际的尺寸。

通常我们感兴趣的是窗口和视口的转换因子。

在MM_LOMETRIC模式下,xViewExt/xWinExt表示每水平方向上每0.01英寸中的像素数,这是系统的一个值。在Win7系统中,控制面板->显示->设置自定义文本大小中,当选择为100%时,每英寸有96个像素。所以比例为:96/100。可以看出,转换因子是不变的。

有一点需要特别注意,就是这几种模式的坐标系相当于把笛卡尔坐标系的原点搬到了左上角,意味着你看到的y轴都是负的。比如:

SetMapMode (hdc, MM_LOENGLISH) ;
        
TextOut (hdc, 100, -100, "Hello", 5)

将把文字显示在距离显示区域左边和上边各一英寸的地方。

“自作主张的”的映射方式

剩下映射方式只有MM_ISOTROPIC和MM_ANISOTROPIC。它们可以改变视口到窗口的转换因子。
MM_ISOTROPIC表示各向同性,表明水平和垂直方向的转换因子是相同的。这意味着,如果你在客户区上画一个矩形,随着客户区的变大变小,矩形也会变大变小,而且矩形的长宽比是不变的。
MM_ANISOTROPIC表示各向异性,表明水平和垂直方向的转换因子是不同的。我们下面具体讨论一下这两种方式:

MM_ISOTROPIC
当您刚开始将映像方式设定为MM_ISOTROPIC时,Windows使用与MM_LOMETRIC同样的窗口和视端口范围。(注意y轴是负的)

您可以用所期望的逻辑窗口的逻辑尺寸作为SetWindowExtEx的参数,用客户区的实际宽和高作为SetViewportExtEx的参数。Windows在调整这些范围时,必须让逻辑窗口适应实际窗口,这就有可能导致客户区的一段落到了逻辑窗口的外面。所以必须在呼叫SetViewportExtEx之前呼叫SetWindowExtEx。

MM_ANISOTROPIC

在MM_ISOTROPIC映像方式下设定窗口和视端口范围时,Windows会调整范围,以便两条轴上的逻辑单位具有相同的实际尺度。在MM_ANISOTROPIC映射方式下,Windows不对您所设定的值进行调整,这就是说,MM_ANISOTROPIC不需要维持正确的纵横比。

这里举一个例子:

#define LOGWIDE 4000
 #define LOGHIGH 3000
case WM_PAINT:
		hdc = BeginPaint(hwnd,&ps);

		//设置映射模式
		
		SetMapMode(hdc,MM_ANISOTROPIC);
		//SetWindowExtEx(hdc,1,1,NULL);		
		//SetViewportExtEx(hdc,1,-1,NULL);
		SetWindowExtEx(hdc,LOGWIDE,LOGHIGH,NULL);

		SetViewportExtEx(hdc,cxClient,-cyClient,NULL);

		SetViewportOrgEx(hdc,0,cyClient /2,NULL);
		//画一条直线
		MoveToEx	(hdc,0,			0,NULL);
		LineTo		(hdc,LOGWIDE,	0);
		MoveToEx	(hdc,0,			0,NULL);
		//绘制正弦曲线
		//for(int i = 0;  i< NUM;i++)
		for( i = 0;  i< LOGWIDE;i++)
		{
			//把x轴等分成1000份
			//apt[i].x = i * cxClient / NUM;
			apt[i].x = i;
			//apt[i].y = (int) (cyClient / 2 * (sin(TWOPI * i /NUM)));
			apt[i].y = (int) (LOGHIGH / 2 * sin(TWOPI * i /LOGWIDE));
			LineTo(hdc,apt[i].x,apt[i].y);
			//Sleep(10);
			
		}		

		EndPaint(hwnd,&ps);
		return 0;

窗口范围为(4000,,3000),而视口范围(1,1).这是什么意思呢?我们可以从两个方面理解:

1.比例关系:4000:客户区大小(比如800)意味着每5个逻辑单位映射为1个像素

2.在逻辑坐标下选一个范围(宽*高),设备坐标下也有一个范围(客户区的宽*高),窗口范围里的东西严格按比例映射到视口。这个理解方法也是我们通常使用的。

想到这里,我觉得微软设计的这个窗口是很好用的。举一个例子,比如股市软件。我们可以保存每一天的信息,然后当我们需要分析哪一段时间之后,我们可以选定一个窗口范围,这个范围内的数据是我们需要的那一段,然后把它展示出来。这时我不要考虑具体是怎么展示的,系统会通过映射模式自动帮你把窗口的内容转化到视口上显示出来。