4.4GDI基本图形

设备环境

设备环境是Windows内部使用的数据结构,它定义了GDI函数在显示设备特定区域的工作方式。对视频显示器来说,设备环境代表屏幕上的一块区域。要想向某个区域输出文字或绘制图形,必须先取得代表此区域的设备环境句柄。以此句柄为参数调用的GDI函数都是对该区域的操作。例如下面代码将在窗口客户区绘制文本。

	case WM_PAINT: // 窗口客户区需要重画
	{
		char szText[] = "大家好";
		HDC hdc;
		PAINTSTRUCT ps;

		// 使无效的客户区变的有效,并取得设备环境句柄
		hdc = ::BeginPaint(hwnd, &ps);
		// 显示文字
		::TextOut(hdc, 10, 10, szText, strlen(szText));
		::EndPaint(hwnd, &ps);
		return 0;
	}

BeginPaint函数取得窗口客户区无效区域的设备环境句柄,所以在上面的代码中,TextOut函数只能在窗口的客户区输出文本。输出结果如图4.7所示。
在这里插入图片描述
如果取得的设备环境句柄对应着整个窗口,而不仅仅是窗口的客户区,就可以在窗口的非客户区绘制文本。请看下面的代码。

case WM_LBUTTONDOWN: 
{ 
     char szText[] = "大家好"; 
     // 取得整个窗口的设备环境句柄
     HDC hdc = ::GetWindowDC(hwnd); 
     ::TextOut(hdc, 10, 10, szText, strlen(szText)); 
     // 释放设备环境句柄
     ::ReleaseDC(hwnd, hdc); 
     return 0; 
}

当在窗口的客户区单击鼠标的左键时,Windows会向该窗口的消息处理函数发送WM_LBUTTONDOWN消息,上面就是响应此消息的代码。

GetWindowDC函数能够取得整个窗口的设备环境句柄,而不仅仅是窗口的客户区,所以以这个设备环境句柄为参数的话,GDI函数就可以对整个窗口区域进行操作,图4.8显示了运行上述代码并在窗口客户区单击鼠标左键后程序的输出。
在这里插入图片描述

如果不是在处理WM_PAINT消息,可以使用GetDC 函数取得窗口客户区的设备环境句柄,进而进行绘制操作,如下代码所示。

hDC = ::GetDC(hWnd); 
//...... // 进行绘制
::ReleaseDC(hWnd, hDC);

这些调用与BeginPaint和 EndPaint 的组合之间的基本区别是,利用从GetDC传回的句柄可以在整个客户区上,不只是在客户区的无效区域绘图。当然,GetDC和 ReleaseDC不使客户区中任何可能的无效区域变成有效。例如,在下面的例子中,程序首先取得记事本程序的窗口句柄,然后通过GetDC函数取得其客户区设备环境句柄,在上面绘制文本。

UseDC.cpp

// 04UseDC 工程下。当然,也可以是一个控制台程序
// UseDC.cpp文件
#include <windows.h>

int APIENTRY WinMain(HINSTANCE hInstance,
	HINSTANCE hPrevInstance,
	LPSTR     lpCmdLine,
	int       nCmdShow)
{
	HDC hdc;
	HWND hWnd;
	char sz[] = "大家好";

	// 查找记事本程序的窗口句柄
	hWnd = ::FindWindow("Notepad", NULL);

	// 如果记事本程序在运行,就向其客户区绘制文本
	while (::IsWindow(hWnd))		// IsWindow函数用于判断一个窗口句柄是否有效
	{
		hdc = ::GetDC(hWnd);
		::TextOut(hdc, 10, 10, sz, strlen(sz));
		::ReleaseDC(hWnd, NULL);
		::Sleep(1000);
	}
	::MessageBox(NULL, "记事本程序已经退出", "04UseDC", MB_OK);

	return 0;
}

打开记事本程序,运行上面的代码,程序就会在记事本的客户区绘制文本,如图4.9所示。
在这里插入图片描述

设备环境结构里除了包含它所代表区域的位置和大小信息外,还包含了绘制图形需要的所有其他属性信息,比如,在输出文本时使用的字体、画图时使用的画笔、删除背景时使用的刷子、选用的坐标系统等。Windows并不允许直接存取设备环境结构中成员的值,而是提供了一些API函数来改变里面的默认值。比如可以用SetTextColor 函数改变DC结构中的文本颜色,用SetBkColor 函数改变DC结构中的文本背景颜色等。例如,下面的代码在处理左键单击事件的时候,将输出文本的颜色设为红色,背景色设为蓝色。

HDC hdc; 
char sz[] = "大家好"; 
switch(message) 
{
case WM_LBUTTONDOWN: 
      hdc = ::GetDC(hWnd); 
      // 设置DC 结构中的文本颜色为红色(下一小节我们再介绍Windows 下的颜色)
      ::SetTextColor(hdc, RGB(255, 0, 0)); 
     // 设置DC 结构中的文本背景颜色为蓝色
      ::SetBkColor(hdc, RGB(0, 0, 255)); 
      ::TextOut(hdc, 10, 10, sz, strlen(sz)); 
      ::ReleaseDC(hWnd, hdc); 
      break; 
case WM_PAINT: 
      PAINTSTRUCT ps; 
      hdc = ::BeginPaint(hWnd, &ps); 
      ::TextOut(hdc, 10, 10, sz, strlen(sz)); 
      ::EndPaint(hWnd, &ps); 
      break; 
      //...... // 处理其它的消息
}

在处理两个消息的时候,在窗口客户区同样的位置输出了同样的字符串。运行程序后,单击鼠标左键前后输出效果的变化如图4.10所示。
在这里插入图片描述

响应WM_LBUTTONDOWN消息的时候,通过一些API函数改变了DC结构中成员的值。但是,紧接着如果使另外一个窗口覆盖字串“大家好”,然后再使这个字符串显示,促使窗口接受到WM_PAINT消息,会发现字符串的文本颜色和背景色又都恢复了。这说明DC结构成员的设置并没有被保存下来,下一次使用GDI函数取得的设备环境仍然是Windows默认的。要想让 Windows将每次对DC的设置都保存下来,只要在注册窗口类时,向WNDCLASSEX结构的style成员添加一个CS_OWNDC标志就可以了,即:

wcex.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;

Windows 的颜色和像素点

DC上的图形和文本都是由像素点组成的。内存中,用颜色的取值来表示象素点。色深指的是存储每个象素所用的位数。一般现在使用的都是24位色,即用24位表示一个像素,这样可以表示的颜色数目为22方种。每种颜色都可以分为红、绿、蓝三原色,所以可以用红、绿、蓝3分量的组合来表示一种颜色,每个分量占用8位就可以了。

在Win32编程中,统一使用32位的整数(一个COLORREF值)来表示深度为24位的颜色。在这32位中只使用低24位,每一种颜色分量占用8位,其中0~7位为红色,8~15位为绿色,16~23 位为蓝色,如图 4.11 所示。
在这里插入图片描述

可以使用 RGB 宏将 3 个分量的值组合在一起。这个宏的定义如下。

#define RGB(r,g,b) ((COLORREF)(((BYTE)(r)|((WORD)((BYTE)(g))<<8))|(((DWORD)(BYTE)(b))<<16)))

r、g、b三个参数分别表示红、绿、蓝的值,例如RGB(255,0,0)表示红色,RGB(0,0,0)表示白色,RGB(255,255,255)表示黑色等。宏GetRValue、GetGValue和 GetBValue可以从COLORREF值中抽出各分量的原色值,这给编程者设置或获取像素的值带来了许多方便。

要想设置一个象素点的值可以用SetPixel函数。

COLORREF SetPixel( 
  HDC hdc, // 设备环境句柄
  int X, // 象素的X 坐标
  int Y, // 象素的Y 坐标
  COLORREF crColor // 要设置的COLORREF 值
);

SetPixel函数可以在hdc的(X,Y)位置以crColor为颜色画上一个像素点。如果需要获取DC中某个像素点当前的颜色值,可以使用GetPixel函数。

COLORREF GetPixel( HDC hdc, int nXPos, int nYPos);

绘制线条

绘制线条的函数有画单条直线的LineTo,画多条直线的Polyline和 PolylineTo,画贝塞儿曲线的PolyBezier和 PolyBezierTo,画弧线的Arc和ArcTo。

绘制直线仅仅需要指定线的开始坐标,然后以线的另一头的坐标为参数调用LineTo函数就即可。例如,下面的代码绘制了一条坐标从(O,0)到(0,500)的直线。

::MoveToEx(hDC, 0, 0, NULL); 
::LineTo(hDC, 0, 500);

DC的数据结构中有一个“当前点”,LineTo函数就是从当前点画一条直线到参数中指定的点,并把这个点设置为新的当前点。画线函数中所有以To结尾的函数都是从当前点开始绘画的,如LineTo、PolylineTo、PolyBezierTo等。而其余的不带To的函数则与当前点没有关系,当然也不会影响当前点的位置。

如果要设置当前点的位置,可以使用MoveToEx函数。

BOOL MoveToEx( 
  HDC hdc, // 设备环境句柄
  int X, // 新位置的X 坐标 
  int Y, // 新位置的Y 坐标
  LPPOINT lpPoint // 用来返回原来的当前点位置,如果不需要的话,这个参数可以被设为NULL
); 

上面绘制直线的代码配合使用了MoveToEx和 LineTo函数。首先由MoveToEx 函数设置一个当前点做为起始坐标,然后用LineTo 函数绘画到结束坐标。

下面的程序代码在窗口的客户区绘制了一个网格,各线的间距为50像素,如图4.12所示。

case WM_PAINT: 
      HDC hdc; 
      PAINTSTRUCT ps; 
      RECT rt; 
      int x, y; 
      hdc = ::BeginPaint(hWnd, &ps); 
      // 取得窗口客户区的大小
      ::GetClientRect(hWnd, &rt); 
      // 画列
      for(x = 0; x < rt.right - rt.left; x += 50) 
      { 
          ::MoveToEx(hdc, x, 0, NULL); 
          ::LineTo(hdc, x, rt.bottom - rt.top); 
      } 
     // 画行
     for(y = 0; y < rt.bottom - rt.top; y += 50) 
     { 
          ::MoveToEx(hdc, 0, y, NULL); 
          ::LineTo(hdc, rt.right - rt.left, y); 
     } 
     ::EndPaint(hWnd, &ps); 
     break;

在这里插入图片描述

GetClientRect函数用于取得指定窗口的客户区的大小,其返回信息在一个RECT结构里。

如果要绘制的是相连的多条直线,可以使用Polyline和 PolylineTo函数。

BOOL Polyline(HDC hdc, CONST POINT *lpPoint, int cPoints); 
BOOL PolylineTo(HDC hdc, CONST POINT *lpPoint, DWORD cPoints );

lpPoint 是指向POINT类型的数组的指针,cPoints参数指出了数组的大小。Polyline 函数只是把 lpPoint 指向的各点连在一起,而 PolylineTo 函数是从当前点开始画线,然后再连接lpPoint指向的各点。所以,如果传递相同的参数,PolylineTo函数画的线比 Polyline函数要多一条。

用Polyline 函数绘制解析函数的图形最方便了。下面是绘制正弦函数y = sin(x)的图形的例子。和用手工画图的步骤一样,先找出此函数上一系列点的坐标,再用“平滑”的曲线将这些坐标连起来。虽然Polyline函数只能用直线连接两个点,但是如果取大量的点的话,最后连成的图形看起来也是平滑的,运行效果如图4.13所示。

SineWave.cpp

//04SineWave 工程下
// SineWave.cpp文件
#include <windows.h>
#include <math.h>

LRESULT __stdcall WndProc(HWND, UINT, WPARAM, LPARAM);

int __stdcall WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
	char szWindowClass[] = "SineWave";

	// 注册窗口类
	WNDCLASSEX wcex;

	wcex.cbSize = sizeof(WNDCLASSEX);
	wcex.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
	wcex.lpfnWndProc = (WNDPROC)WndProc;
	wcex.cbClsExtra = 0;
	wcex.cbWndExtra = 0;
	wcex.hInstance = hInstance;
	wcex.hIcon = NULL;
	wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
	wcex.hbrBackground = (HBRUSH)::GetStockObject(WHITE_BRUSH);
	wcex.lpszMenuName = NULL;
	wcex.lpszClassName = szWindowClass;
	wcex.hIconSm = NULL;

	::RegisterClassEx(&wcex);


	// 创建并线程主窗口
	HWND hWnd = ::CreateWindowEx(
		WS_EX_CLIENTEDGE,	// 扩展样式
		szWindowClass,		// 类名
		"正弦线",		// 标题
		WS_OVERLAPPEDWINDOW,	// 窗口样式
		CW_USEDEFAULT,	// 初始 X 坐标
		CW_USEDEFAULT,	// 初始 X 坐标
		CW_USEDEFAULT,	// 宽度
		CW_USEDEFAULT,	// 高度
		NULL,		// 父窗口句柄
		NULL,	        // 菜单句柄
		hInstance,	// 程序实例句柄
		NULL);

	::ShowWindow(hWnd, nShowCmd);
	::UpdateWindow(hWnd);

	// 进入消息循环
	MSG msg;
	while (::GetMessage(&msg, NULL, 0, 0))
	{
		::TranslateMessage(&msg);
		::DispatchMessage(&msg);
	}
	return 1;
}

// 消息处理函数    // 03SineWave工程下
LRESULT __stdcall WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
#define SEGMENTS 500  // 取的点数(在一个周期内取500个点)
#define PI 3.1415926  // 圆周率

	HDC hdc;
	PAINTSTRUCT ps;
	RECT rt;
	int cxClient, cyClient;
	POINT pt[SEGMENTS];
	int i;

	switch (message)
	{
	case WM_PAINT:
		hdc = ::BeginPaint(hWnd, &ps);
		::GetClientRect(hWnd, &rt);
		cxClient = rt.right - rt.left;
		cyClient = rt.bottom - rt.top;

		// 画横坐标轴
		::MoveToEx(hdc, 0, cyClient / 2, NULL);
		::LineTo(hdc, cxClient, cyClient / 2);
		// 找出500个点的坐标
		for (i = 0; i < SEGMENTS; i++)
		{
			pt[i].x = cxClient * i / SEGMENTS;
			pt[i].y = (int)((cyClient / 2)*(1 - sin(2 * PI*i / SEGMENTS)));
		}
		// 将各点连在一起
		::Polyline(hdc, pt, SEGMENTS);

		::EndPaint(hWnd, &ps);
		break;
	case WM_DESTROY:
		::PostQuitMessage(0);
		break;
	}
	return ::DefWindowProc(hWnd, message, wParam, lParam);
}

在这里插入图片描述
一个周期内x轴上的取值是0到2n,上面的代码将2T平均分成了500份,所以最后得到了正弦线上500个点。比较麻烦的是y轴的坐标变换过程。必须要明白,在默认的坐标系中,y轴的正方向是向下的,而数学上y轴的正方向是向上的,所以要对计算结果取反。为了将坐标原点上移到窗口的左上角,又要加上cyClient/2,即。

pt[i].y = (int)((cyClient / 2)*(1 - sin(2 * PI*i / SEGMENTS)));

仔细研究上面的代码能够使大家更深刻地了解计算机绘图。

既然是绘画,就要用到画笔,所以在 DC结构中还有一个画笔对象句柄,每次绘画时,GDI函数都会去使用此句柄指示的画笔。画笔对象规定了线条的宽度、颜色和风格。

要想改变DC中默认的画笔对象,可以使用 Windows预定义的画笔对象,也可以创建新的画笔对象。预定义的画笔对象很简单,仅有BLACK_PEN、WHITE_PEN、NULL_PEN三种,分别是黑色画笔、白色画笔和空画笔。在代码中使用它们的一般格式如下。

// 获取预定义画笔的句柄。Stock 的中文含义是常备的、库存的
HPEN hPen = (HPEN)::GetStockObject(BLACK_PEN); 
// 将画笔对象选入设备。SelectObject 函数会根据句柄的种类自动替换掉原来的对象,并返回原对象句柄
HPEN hOldPen = (HPEN)::SelectObject(hdc, hPen); 
//...... // 开始在DC 中绘图

用GetStockObject得到对象的句柄以后,就可以使用SelectObject将对象选入到DC中了。预定义的画笔对象太“简陋”了,只能是白色或黑色的宽度为1个像素的实线。使用CreatePen函数可以创建自定义的画笔对象。

HPEN CreatePen( 
 int fnPenStyle, // 画笔的风格,取值有PS_SOLID、PS_DASH、PS_DOT、PS_DASHDOT 等
 int nWidth, // 画笔的宽度,单位是DC 坐标映射方法中定义的逻辑单位
 COLORREF crColor // 画笔的颜色
);

例如,下面的代码绘制了一条宽度为 3 个像素的红色线条。

HPEN hPen = ::CreatePen(PS_SOLID, 3, RGB(255, 0, 0)); 
HPEN hOldPen = (HPEN)::SelectObject(hdc, hPen); 
::MoveToEx(hdc, 0, 100, NULL); 
::LineTo(hdc, 500, 100); 
::SelectObject(hdc, hOldPen); 
::DeleteObject(hPen); // 一定要删除上面创建的画笔对象,以释放资源

绘制区域

绘制边线的时候要使用画笔,填充区域就要使用画刷了。绘制区域的函数工作的时候以当前画笔绘制边线,并以当前画刷填充中间的区域。这些函数有画矩形的 Rectangle,画椭圆的Ellipse,画多边形的Polygon,画弦的Chord等,其用法如下。

Rectangle(hdc, x1, y1, x2, y2); // 画以(x1, y1)和(x2, y2)为对角坐标的填充矩形
Ellipse(hdc, x1, y1, x2, y2); // 以(x1, y1)和(x2, y2)为对角坐标定义一个矩形,然后画矩形相切的椭
                             // 圆并填充
Polygon(hdc, lpPoint, 5) // lpPoint 指向存放(x0, y0)到(x4, y4)的内存,函数从(x1, y1)到(x2, y2)...
                        // 到(x4, y4),再回到(x1, y1),一共画5 条直线并填充

函数的用法相当简单,比如下面是响应WM_PAINT消息的函数,它在窗口客户区中心绘制了100×100像素的矩形,并用红色画刷填充。

void OnPaint(HWND hWnd) // 你要在接受到WM_PAINT 消息的时候调用此函数
{ 
    RECT rt; ::GetClientRect(hWnd, &rt); // 注意,我为了节省空间才写在一起,你不要这样做
    int xCenter = rt.right/2; 
    int yCenter = rt.bottom/2; 
    PAINTSTRUCT ps; 
    HDC hdc = ::BeginPaint(hWnd, &ps); 
    // 创建一个单色(红色)的刷子并选入设备
    HBRUSH hBrush = ::CreateSolidBrush(RGB(255, 0, 0)); 
    HBRUSH hOldBrush = (HBRUSH)::SelectObject(hdc, hBrush); 
    // 以 xCenter、yCenter 为中心,画一个边长为100 的正方形
    ::Rectangle(hdc, xCenter - 50, yCenter + 50, xCenter + 50, yCenter -50); 
    ::SelectObject(hdc, hOldBrush); 
    ::DeleteObject(hBrush); 
    ::EndPaint(hWnd, &ps); 
}

如果对这个矩形的边框不满意,也可以自己创建画笔对象,再选入DC中。上面的代码使用CreateSolidBrush函数创建了新的单色的画刷对象,以取代 DC中默认的画刷对象。

CreateSolidBrush函数要输入的惟一的参数是画刷的颜色,而使用CreateHatchBrush 函数创建的画刷还可以带有特定风格的线条。

HBRUSH CreateHatchBrush( 
  int fnStyle, // 线条的风格
  COLORREF clrref // 图案线条的颜色
);

fnStyle参数指定了不同风格的线条,这些图案线条实际上是由8×8的位图重复铺开组成的。此参数取值可以是HS_BDIAGONAL、HS_FDIAGONAL、HS_CROSS、HS_HORIZONTAL、HS_DIAGCROSS或HS_VERTICAL,其对应的图案如图4.14所示。
在这里插入图片描述
当然,Windows也预定义了一些画刷对象,可以被 GetStockObject函数获取,并被选入DC中。这些常用的预定义对象有BLACK_BRUSH(黑色画刷)、DKGRAY_BRUSH(深灰色画刷)、GRAY_BRUSH(灰色画刷)、LTGRAY_BRUSH(浅灰色画刷)、WHITE_BRUSH(白色画刷)、NULL_BRUSH(空画刷)等。

坐标系统

坐标映射方式是设备环境中一个很重要的属性。它的默认值是MM_TEXT,即以左上角为坐标原点,以右方当做x坐标的正方向,以下方当做y坐标的正方向。这种坐标系使用的单位是像素,其好处是窗口中每一点的坐标不会因为窗口的大小而改变。前面的例子使用的都是默认坐标系。

可以用SetMapMode函数改变DC中的坐标映射方式。

int SetMapMode( 
  HDC hdc, // 设备环境句柄
  int fnMapMode // 新的坐标映射方式
);

Windows一共支持8种不同的坐标映射方式,表4.1中对它们各自的属性做了总结。最后两种映射方式提供了更灵活的选择。这个时候可以自己设置逻辑单位,坐标系的原点和坐标的正方向等参数。
在这里插入图片描述在这里插入图片描述
必须经过一系列函数的调用才能确定坐标系。首先调用SetWindowExtEx函数设置坐标系的逻辑单位。

BOOL SetWindowExtEx( 
  HDC hdc, // 设备环境句柄
  int nXExtent, // 新的宽度(映射方式为MM_ISOTROPIC 时nXExtent 必须等于nYExtent)
  int nYExtent, // 新的高度
  LPSIZE lpSize // 用于返回原来的大小,不需要的话可以设它为NULL 
);

不管真实的区域大小是多少,仅仅使用这个函数告诉 Windows此区域的逻辑宽度为nXExtent,逻辑高度为nYExtent。Windows会将DC所代表区域的宽度做nXExtent等分,每份的长度就是x轴方向的一个单位长度,将高度做nYExtent 等分就得到了y轴的单位长度。

设置了逻辑单位以后,还要调用SetViewportExtEx函数设置x、y坐标轴的方向和坐标系包含的范围,即定义域和值域。

BOOL SetViewportExtEx( 
 HDC hdc, // 设备环境句柄
 int nXExtent, // 新的宽度(以像素为单位,定义域)
 int nYExtent, // 新的高度(以像素为单位,值域)
 LPSIZE lpSize // 用于返回原来的大小,不需要的话可以设它为NULL 
);

要想坐标系包含整个区域,直接将区域的真实的大小传给此函数就可以了。参数 nXExtent和nYExtent为正则表示x、y轴的正方向和默认的坐标系相同,即向右和向下分别为x、y轴的正方向,为负则表示与默认方向相反。

最后别忘了设置坐标系的原点坐标。

BOOL SetViewportOrgEx( 
 HDC hdc, // 设备环境句柄
 int X, // 原点的横坐标
 int Y, // 原点的纵坐标
 LPPOINT lpPoint // 用于返回原来的坐标,不需要的话可以设它为NULL 
);

当绘制的图形需要随着窗口的大小改变而自动改变的时候,使用MM_ISOTROPIC和MM_ANISOTROPIC坐标映射方式最合适了。下面的代码使用MM_ANISOTROPIC坐标映射方式画了一个与窗口的各边都相切的椭圆。

void OnPaint(HWND hWnd) // 你要在接受到WM_PAINT 消息的时候调用此函数
{ 
     PAINTSTRUCT ps; 
     HDC hdc = ::BeginPaint(hWnd, &ps); 
     // 取得客户区的大小
     RECT rt; ::GetClientRect(hWnd, &rt); // 注意,我为了节省空间才写在一起,你不要这样做
     int cx = rt.right; 
     int cy = rt.bottom; 
     // 设置客户区的逻辑大小为500 乘500,原点为(0,0)
     ::SetMapMode(hdc, MM_ANISOTROPIC); 
     ::SetWindowExtEx(hdc, 500, 500, NULL); 
     ::SetViewportExtEx(hdc, cx, cy, NULL); 
     ::SetViewportOrgEx(hdc, 0, 0, NULL); 
     // 以整个客户区为边界画一个椭圆
     ::Ellipse(hdc, 0, 0, 500, 500); 
     ::EndPaint(hWnd, &ps); 
}

看看它是怎么工作的?不管真实窗口的大小是多少,程序已经告诉了Windows 该窗口的逻辑大小是500×500,因此,程序运行后一个椭圆总是会环绕在整个窗口中。

MM_ISOTROPIC和MM_ANISOTROPIC映射方式的唯一区别就是,在前面的方式里,X轴和¥轴的逻辑单位的大小是相同的。Isotropic就是“各方向相等”的意思,此映射方式对于绘制圆或正方形来说非常合适。

下面的自定义函数SetIsotropic将坐标系设置成了数学上常用的笛卡儿坐标系,其原点在区域的中心,向右、向上分别为x轴和y轴的正方向。

void SetIsotropic(HDC hdc, int cx, int cy) 
{ 
    ::SetMapMode(hdc, MM_ISOTROPIC); 
    ::SetWindowExtEx(hdc, 1000, 1000, NULL); 
    ::SetViewportExtEx(hdc, cx, -cy, NULL); // 第 3 个参数为负值,说明了Y 轴的正方向与默认方向相反
    ::SetViewportOrgEx(hdc, cx/2, cy/2, NULL); 
}

在这里插入图片描述

使用的时候,应该将参数cx、cy 的值设置为 hdc所代表区域的真实大小。在这个坐标系中进行坐标变换,绘制数学图形就很简单了。例如,本章的实例Clock 里面有一个画时钟外观的函数 DrawClockFace,它将在窗口客户区里画12个黑圆圈。这个函数的实现就是在上面的坐标系下完成的。

// 绘制时钟的外观
void DrawClockFace(HDC hdc) 
{ 
     const int SQUARESIZE = 20; 
     static POINT pt[] = 
     { 
           0, 450, // 12 点
           225, 390, // 1 点
           390, 225, // 2 点
           450, 0, // 3 点
           390, -225, //... 下面的坐标是上面的点的对称点(关于X 轴、Y 轴或原点对称)
           225, -390, 
           0, -450, 
           -225, -390, 
           -390, -225, 
           -450, 0, 
           -390, 225, 
           -225, 390 
     }; 
     // 选择一个黑色的画刷
     ::SelectObject(hdc, ::GetStockObject(BLACK_BRUSH)); 
     // 画 12 个圆
     for(int i=0; i<12; i++) 
     { 
         ::Ellipse(hdc, pt[i].x - SQUARESIZE, pt[i].y + SQUARESIZE, 
                   pt[i].x + SQUARESIZE, pt[i].y - SQUARESIZE); 
     } 
}

在处理WM_PAINT消息时使用下面的代码就可以看到程序的运行效果,如图4.15所示。

hdc = ::BeginPaint(hWnd, &ps); // 定义变量的代码我已经省略
::GetClientRect(hWnd, &rt); 
SetIsotropic(hdc, rt.right - rt.left, rt.bottom - rt.top); 
DrawClockFace(hdc); 
::EndPaint(hWnd, &ps);

DrawClockFace函数中最关键的部分是得到各刻度点的中心坐标。在代码中直接给出的pt数组的值很容易通过计算取得。函数在绘制图形时使用的坐标系如图4.16所示。
在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阳光开朗男孩

你的鼓励是我最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值