/**********************************************************************
*
* 这一篇文是以前写的,但现在看来好像还是没有透彻理解。一块内容真的需要反反复复好几遍才能理解。
*
************************************************************************/
对于我们来说,都学过笛卡尔坐标系,无论画什么图都首先考虑用笛卡尔坐标系来描述,比如说,我想在x=100,y=200的地方画一个点,我的脑中想像的是这样的:
可是,一旦把【想像】的东西画在程序的客户区中,就有变化了,如果想得到同样的点,前提是程序客户区的坐标系和这一样,也是笛卡尔坐标系,方向也相同,x轴向右为正,y轴向上为正。一个点可能未必那么明显,假设想让别人看到三个点的图像,(这三个点的图像是想像的,也是想让别人看到原样的图像),比如:
三个点的坐标,假设为(100,200),(200,300),(300,400),(不改变设备任何属性)用API的设点函数,代码:SetPixel(hdc,100,200,RGB(0,0,0));SetPixel(hdc,200,300,RGB(0,0,0));SetPixel(hdc,300,400,RGB(0,0,0));
(圈住的就是点),由这个图可以看出,走形了,已经不是想让别人看到的那个图。这是为什么呢?其实就是因为客户区的坐标系和习惯用的笛卡尔坐标系不同了。客户区的坐标系是下面这样的:
这样一来,就会导致出现上面“走形”的图像。矛盾就出现在这儿,我们习惯于笛卡尔坐标系来描述一幅图,而windows用的是这种坐标系,接下来会考虑怎样把windows的这种坐标系转个方向变成笛卡尔坐标系,适应自己的习惯!这里就出现了映射模式,在默认情况下,客户区的坐标参考系为上图所示,名称为MM_TEXT,X轴向右为正,Y轴向下为正,除此之外,还有其它几种映射模式。
现在,将客户区的坐标系改成笛卡尔坐标系,SetMapMode(hdc,MM_LOMETRIC),单位为0.1mm。得到下面这种样式:这样就得到了自己习惯的坐标系,接下来,会出现这样一个问题,假如说,我想画一个(类似)正弦的弧线,下图所示,我想从X的负坐标开始(这些都是习惯),可是上图所示的坐标系的原点是在客户区的左上角,还是不太习惯,能不能把这个坐标原点移到中间,把上面和左边都留出一定的空隙,这样看起来更适应习惯。
于是,又出现一个函数,可以将这个原点进行移动。
如上图所示,把坐标原点从客户区左上角移到客户区的(100,150)的位置,经过截图软件测距,观察得到的确实是x=100,y=150,这两个值是设备单位,也就是像素。
代码:SetMapMode(hdc,MM_LOMETRIC);SetViewportOrgEx(hdc,100,150,NULL);
这里采用的函数是:SetViewportOrgEx(),经过上面两步,首先将坐标系改变,接着再将坐标原点移动,目的只有一个:得到一个适应自己习惯的坐标系。
有了这个坐标系之后,就可以在上面进行操作,但是这个坐标系的逻辑单位是0.1mm,假如画一条线,代码写100,其实是1厘米。
//这一段尤其重要,当转换映射模式后,在绘图函数中,它默认的是当前映射模式下的单位,比如,当前是MM_LOMETRIC,则画线函数中采用的400,是以0.1mm为单位,而不是以像素为单位
打算从坐标原点开始画两条直线,已经具有了笛卡尔坐标系,这样【想像】的是什么,画出来的就是原样的外观。
MoveToEx(hdc,0,0,NULL);LineTo(hdc,300,0);MoveToEx(hdc,0,0,NULL); LineTo(hdc,0,400);//y轴400
一个完整的示例:
代码(注意:用win32应用程序创建一个典型的"hello world"模板。)
// 正弦.cpp : Defines the entry point for the application.//#include "stdafx.h"#include "resource.h"#include "math.h"#define PI 3.1415#define MAX_LOADSTRING 100// Global Variables:HINSTANCE hInst; // current instanceTCHAR szTitle[MAX_LOADSTRING]; // The title bar textTCHAR szWindowClass[MAX_LOADSTRING]; // The title bar text// Foward declarations of functions included in this code module:ATOM MyRegisterClass(HINSTANCE hInstance);BOOL InitInstance(HINSTANCE, int);LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);LRESULT CALLBACK About(HWND, UINT, WPARAM, LPARAM);int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow){ // TODO: Place code here. MSG msg; HACCEL hAccelTable; // Initialize global strings LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING); LoadString(hInstance, IDC_MY, szWindowClass, MAX_LOADSTRING); MyRegisterClass(hInstance); // Perform application initialization: if (!InitInstance (hInstance, nCmdShow)) { return FALSE; } hAccelTable = LoadAccelerators(hInstance, (LPCTSTR)IDC_MY); // Main message loop: while (GetMessage(&msg, NULL, 0, 0)) { if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) { TranslateMessage(&msg); DispatchMessage(&msg); } } return msg.wParam;}//// FUNCTION: MyRegisterClass()//// PURPOSE: Registers the window class.//// COMMENTS://// This function and its usage is only necessary if you want this code// to be compatible with Win32 systems prior to the 'RegisterClassEx'// function that was added to Windows 95. It is important to call this function// so that the application will get 'well formed' small icons associated// with it.//ATOM MyRegisterClass(HINSTANCE hInstance){ WNDCLASSEX wcex; wcex.cbSize = sizeof(WNDCLASSEX); wcex.style = CS_HREDRAW | CS_VREDRAW; wcex.lpfnWndProc = (WNDPROC)WndProc; wcex.cbClsExtra = 0; wcex.cbWndExtra = 0; wcex.hInstance = hInstance; wcex.hIcon = LoadIcon(hInstance, (LPCTSTR)IDI_MY); wcex.hCursor = LoadCursor(NULL, IDC_ARROW); wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); wcex.lpszMenuName = (LPCSTR)IDC_MY; wcex.lpszClassName = szWindowClass; wcex.hIconSm = LoadIcon(wcex.hInstance, (LPCTSTR)IDI_SMALL); return RegisterClassEx(&wcex);}//// FUNCTION: InitInstance(HANDLE, int)//// PURPOSE: Saves instance handle and creates main window//// COMMENTS://// In this function, we save the instance handle in a global variable and// create and display the main program window.//BOOL InitInstance(HINSTANCE hInstance, int nCmdShow){ HWND hWnd; hInst = hInstance; // Store instance handle in our global variable hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL); if (!hWnd) { return FALSE; } ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); return TRUE;}//// FUNCTION: WndProc(HWND, unsigned, WORD, LONG)//// PURPOSE: Processes messages for the main window.//// WM_COMMAND - process the application menu// WM_PAINT - Paint the main window// WM_DESTROY - post a quit message and return////LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam){ int wmId, wmEvent; PAINTSTRUCT ps; HDC hdc; TCHAR szHello[MAX_LOADSTRING]; LoadString(hInst, IDS_HELLO, szHello, MAX_LOADSTRING); static HPEN hpen; double y,r; int x; switch (message) { case WM_CREATE: hpen=CreatePen(PS_SOLID,6,RGB(0,0,0)); return 0; case WM_COMMAND: wmId = LOWORD(wParam); wmEvent = HIWORD(wParam); // Parse the menu selections: switch (wmId) { case IDM_ABOUT: DialogBox(hInst, (LPCTSTR)IDD_ABOUTBOX, hWnd, (DLGPROC)About); break; case IDM_EXIT: DestroyWindow(hWnd); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } break; case WM_PAINT: hdc = BeginPaint(hWnd, &ps);SelectObject(hdc,hpen); SetMapMode(hdc,MM_LOMETRIC);//映射模式改变 SetViewportOrgEx(hdc,100,150,NULL); //视区原点改变 for(x=-60;x<600;x++) { r=x/((double)60*2)*PI; y=sin(r)*2*60; MoveToEx(hdc,(int)x,(int)y,NULL); LineTo(hdc,(int)x,(int)y); } MoveToEx(hdc,-240,0,NULL);//横向线 LineTo(hdc,680,0); MoveToEx(hdc,0,400,NULL);//纵向线 LineTo(hdc,0,-380); EndPaint(hWnd, &ps); break; case WM_DESTROY: DeleteObject(hpen); PostQuitMessage(0); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } return 0;}// Mesage handler for about box.LRESULT CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam){ switch (message) { case WM_INITDIALOG: return TRUE; case WM_COMMAND: if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL) { EndDialog(hDlg, LOWORD(wParam)); return TRUE; } break; } return FALSE;}
继续新的问题:
考虑这样一种情况,在MM_LOMETRIC的映射模式下(不改变坐标原点),我想从坐标原点处开始画一条线直接到达窗口的最底端。如图所示(现在研究左边的一条线):
这条线尾的特点是,X不变(固定一个值,比如360),而y也就是客户区的高度可以自由伸缩。要实现这个功能,首先要获取窗口的高度,采用GetClientRect函数。但问题出在这了,这个函数返回的是客户区的矩形区域,它是设备坐标还是逻辑坐标呢?书上的意思是,它返回的是设备坐标,而设备坐标是像素。我这个画线程序里面的360是逻辑坐标(0.1mm为单位),不匹配,所以要将设备坐标转换为逻辑坐标。这就算是DPtoLP函数的一个来历。
GetClientRect (hwnd, &rect) ; //获取窗口客户区的尺寸pt.y=rect.bottom;//高度pt.x=rect.right;//宽度DPtoLP(hdc,(LPPOINT)&pt,2);//转换pt为逻辑坐标
这样一来,两者就匹配了。
LineTo(hdc,360,pt.y);
但是,在这里又出现一个问题,MM_LOMETRIC的坐标系和笛卡尔坐标系一样,现在LineTo(hdc,360,pt.y),这个pt.y看起来像是一个正数,x和y都为正数的点怎么会落在用户区中呢?如果一个东西只有一个解释,那就是唯一的解释,那就是这个pt.y只能是个负数才合理。验证输出pt.y发现它确实是个负数。
DPtoLP将用户区的高度进行了两个功能的转换,第一,根据坐标方向,转换正负值,第二,将设备高转换为逻辑高。
整理一下这个过程,假设用户区没有做任何映射转换,就是MM_TEXT,x向右为正,y向下为正。通过GetClientRect函数得到的高度是像素为单位。接着,转换用户区的映射方式,变成MM_LOMETRIC,通过GetClientRect函数获取高度,则依然是像素,说明,GetClientRect是以设备坐标为单位(像素)返回用户区高度。
既然都为正,那就说明问题出在DPtoLP这个函数头上,它的工作要取决于当前的映射方式,当它发现是MM_LOMETRIC的映射模式时,就将这个高度转换为负值的以0.1mm为单位的逻辑坐标。这样,一切就合理了。根据后文一个粗略的近似计算:16像素约等于5.6mm,我这个程序的窗口约694像素,两者一换算:2429(0.1mm为单位),而程序输出经过DPtoLP转换后的高度为:-2444,几乎相等,这样就证明了。
得到下面两条结论:一、GetClientRect返回的是用户区的矩形高度(设备坐标,像素为单位),和方向无关,总是用设备单位。二、DPtoLP函数会根据当前的映射方式将设备坐标转换为逻辑坐标,正如上面所示,不仅改单位,还改方向。代码示例:
//--------------------DPtoLP函数的研究例子---------------//#include#include "stdio.h"LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;void paint();int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,PSTR szCmdLine, int nCmd){ static TCHAR szAppName[] = TEXT ("HelloWin") ; HWND hwnd ; //窗口句柄 MSG msg ; //消息结构 WNDCLASS wndclass ; //窗口类结构 wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;//加载图标供程序使用 wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; //加载鼠标指针供程序使用 wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;//获取一个图形对象,在这个例子中,是获取绘制窗口背景的刷子 wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass))//为程序窗口注册窗口类 { return 0 ; } //根据窗口类创建一个窗口 hwnd = CreateWindow (szAppName, TEXT ("一个简单的Win32程序"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, SW_SHOWMAXIMIZED) ; //在屏幕上显示窗口 UpdateWindow (hwnd) ; //指示窗口刷新自身 while (GetMessage (&msg, NULL, 0, 0)) //从消息队列中获取消息 { TranslateMessage (&msg) ; //转换某些键盘消息 DispatchMessage (&msg) ; //将消息发送给窗口过程 } return msg.wParam ;}LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam){ HDC hdc ; PAINTSTRUCT ps ; RECT rect ; POINT pt; char a[20],b[20],c[20]; switch (message) { case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; //开始窗口绘制 GetClientRect (hwnd, &rect) ;//转换映射模式前,默认为MM_TEXT pt.y=rect.bottom;//返回用户区高度 sprintf(a,"MM_TEXT's height:%d",pt.y); TextOut(hdc,350,0,a,strlen(a)); //转换映射模式为MM_LOMETRIC SetMapMode(hdc,MM_LOMETRIC); GetClientRect (hwnd, &rect) ; //获取窗口客户区的尺寸 pt.y=rect.bottom;//高度 pt.x=rect.right;//宽度 sprintf(b,"MM_LOMETRIC's height:%d",pt.y);//没有被DPtoLP转换前 TextOut(hdc,0,0,b,strlen(b)); DPtoLP(hdc,(LPPOINT)&pt,2); sprintf(c,"MM_LOMETRIC's height:%d",pt.y);//被DPtoLP转换后 TextOut(hdc,0,-250,c,strlen(c)); LineTo(hdc,360,pt.y); MoveToEx(hdc,360,pt.y,NULL); LineTo(hdc,pt.x,-90); EndPaint (hwnd, &ps) ; //结束窗口绘制 return 0 ; case WM_DESTROY: PostQuitMessage (0) ; //在消息队列中插入一条“退出”消息 return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam);//执行默认的消息处理}
题目:以毫米为单位在用户区中输出(上下相邻)两行字符串,如何解?
如果打印两行字符串,需要在行间保持(一定的)距离才不会重叠或者行距太大。
第一个问题,如何知道字体的高度呢?根据文本显示的研究,字体的高度存放在TEXTMETRIC这个结构中,通过创建DC得到这个高度,接着的问题是,如果按照默认映射模式,也即MM_TEXT模式,得到的是映射模式的逻辑单位,换句话说,假如不改变映射模式,得出的高度为16(象素)。
16是象素,但是现在需要以毫米为单位,显然这个16不符合要求,需要把映射模式改为MM_LOMETRIC,于是得到逻辑单位以0.1mm为单位,再去取TEXTMETRIC中的高度,得到56。
计算:
同样的字体,象素为单位=16,0.1mm为单位=56,于是16象素=5.6mm。通过计算得知,在以MM_LOMETRIC的映射模式下,字体的高度为56(即5.6毫米),问题已经基本得到解决。
在MM_LOMETRIC的映射模式下,x向右,y向上, 假如第一行字串起点为:(100,-100),则第二行字串的起点为(100,-100-56)。
同样,如果转换为MM_TEXT模式,在相同的地方输出,则第一行字串转换为(28,28),可以覆盖,稍有点误差。
代码如下: