研究:窗口映射

/**********************************************************************

*

*  这一篇文是以前写的,但现在看来好像还是没有透彻理解。一块内容真的需要反反复复好几遍才能理解。

*

************************************************************************/

对于我们来说,都学过笛卡尔坐标系,无论画什么图都首先考虑用笛卡尔坐标系来描述,比如说,我想在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 instance
TCHAR szTitle[MAX_LOADSTRING];                                // The title bar text
TCHAR 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 <windows.h>
#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),可以覆盖,稍有点误差。
代码如下:

View Code

 

原文地址:https://www.cnblogs.com/tinaluo/p/5389636.html