一、CDC类(二级缓存)
CDC类是设备上下文的核心类,它的作用是抽象化对图形输出设备(像屏幕、打印机、内存位图等)的访问,借助CDC我们可以在屏幕上绘制图像。
自定义控件中采用了双缓冲技术,防止在绘制图像时屏幕闪烁。其中用到了关键的几个类和函数,如下。
//成员变量
CDC m_ImageDC; // 源图片DC
HBITMAP m_hImageBitmap; // 位图
BITMAPINFO* m_pBitmapInfo;
void* m_pBuffdata; // 图像数据
CDC m_buffDC_image; // 二级缓存
CBitmap m_cBitmap;
图像放在控件上,图像的大小和控件的大小可能不一样,这时需要将图像进行压缩或放绘制到控件上(需要直接操作数据,调用Windows API),故在第一个CDC中使用了HBITMAP
位图。创建位图调用Windows API接口函数 CreateDIBSection()
函数。该函数直接绑定数据指针m_pBuffdata
,可以直接操作数据。
计算好压缩因子、视口、窗口显示区域,通过StretchBlt()
函数进行压缩到m_ImageDC中,再通过BitBlt()
函数将m_ImageDC复制到m_buffDC_image,再将m_buffDC_image复制到屏幕上。
进行放大操作就是调节视口区域,再计算压缩因子进而计算窗口显示区域,再把图像放到指定区域。
二、计算视口
//成员变量
CSize m_tSizeWnd; // 控件显示窗口的尺寸
CSize m_tSizeImg; // 图片的尺寸
int m_iImageSize; // 图片的字节数
CRect m_reShowImage; // 显示图像区域(视口)
CRect m_reShowWnd; // 显示窗口区域
double m_dChangeNum; // 压缩因子(变化因子)
要求:将图像显示在控件上,并且保证在控件上居中。
计算缩放因子
图像不一定是完美契合控件大小,要等比例缩放需计算缩放因子。
计算窗口的宽高和图像宽高的比例,为了图像全部显示在控件上,我们选择最小的比例当缩放因子。
//计算缩放因子
double dChangeX = (double)m_tSizeWnd.cx / m_tSizeImg.cx;
double dChangeY = (double)m_tSizeWnd.cy / m_tSizeImg.cy;
m_dChangeNum = min(dChangeX, dChangeY);
计算视口(图像全部显示在空间上的视口大小)
图像的显示区域就是视口,图像完全显示到控件上,及视口就是整个图像的区域。
//视口大小(相对于图像的坐标)
m_reShowImage.left = 0;
m_reShowImage.top = 0;
m_reShowImage.right = m_reShowImage.left + m_tSizeImg.cx;
m_reShowImage.bottom = m_reShowImage.top + m_tSizeImg.cy;
计算控件显示图像的区域(居中)
第一步:通过压缩因子计算压缩后图像的宽高
第二步:通过控件的宽高-压缩后图像的宽高再除以2,得到居中后的坐标
double dWid = static_cast<double>(m_reShowImage.right - m_reShowImage.left) * m_dChangeNum;
double dHit = static_cast<double>(m_reShowImage.bottom - m_reShowImage.top) * m_dChangeNum;
int iWid = static_cast<int>(dWid + 0.5); // 四舍五入
int iHit = static_cast<int>(dHit + 0.5);
m_reShowWnd.left = (m_tSizeWnd.cx - iWid) >> 1;
m_reShowWnd.top = (m_tSizeWnd.cy - iHit) >> 1;
m_reShowWnd.right = m_reShowWnd.left + iWid;
m_reShowWnd.bottom = m_reShowWnd.top + iHit;
再调用第一节说的StretchBlt()
函数将图像缩放复制到二级缓存中
::StretchBlt(m_buffDC_image.GetSafeHdc(),
m_reShowWnd.left,
m_reShowWnd.top,
m_reShowWnd.Width(),
m_reShowWnd.Height(),
m_ImageDC.GetSafeHdc(),
m_reShowImage.left,
m_reShowImage.top,
m_reShowImage.Width(),
m_reShowImage.Height(),
SRCCOPY);
三、放大操作
放大操作:计算新视口
当鼠标点击图像时,图像放大。鼠标指向的区域是相对图像坐标是不变的,通过这个坐标计算新的视口。
补充:当图像需要放大两倍时,就是视口区域缩小两倍,再显示控件上就是放大两倍的效果。
重写鼠标点击事件,我们可以得到鼠标点击的坐标(x,y)。因为是等比例缩放,通过缩放因子计算可以得到W2和H2。及可以算出坐标到新视口边的距离。
再计算之前需要将坐标转换为图像坐标系上的坐标。(图上画的两个矩形都是图像)
坐标转换(控件上的坐标转换成图像上的坐标)
//将窗口的坐标转化为图像上的坐标
//因为图像是通过缩放因子等比例缩放显示在控件上的,故可以通过这层关系
//计算相对于图像上的坐标
void MyImageView::OnLButtonDblClk(UINT nFlags, CPoint point)
{
point.x /= m_dChangeNum;
point.y /= m_dChangeNum;
}
计算新的缩放因子、计算新的视口大小
double dOldChangeNum = m_dChangeNum;
double newScale = 2 * m_dChangeNum;
newScale = max(m_dMinScale, min(m_dMaxScale, newScale));
double scaleRatio = dOldChangeNum / newScale; // 1/2
//通过新的缩放因子得出新视口的宽高
int iWid = m_reShowImage.Width() * scaleRatio;
int iHit = m_reShowImage.Height() * scaleRatio;
//通过比例关系 计算出视口的区域坐标
m_reShowImage.left = point.x - (point.x * iWid / m_tSizeImg.cx);
m_reShowImage.top = point.y - (point.y * iHit / m_tSizeImg.cy);
m_reShowImage.right = m_reShowImage.left + iWid;
m_reShowImage.bottom = m_reShowImage.top + iHit;
至此计算出新视口的区域,再进行计算新显示窗口区域就可以完成放大操作。
代码中初始化操作(方便以后cv)
CDC* pDC = pWnd->GetDC();
m_pBitmapInfo = (BITMAPINFO*)malloc(sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * 255);
if (NULL != m_pBitmapInfo)
{
for (int i = 0; i < 256; i++)
{
m_pBitmapInfo->bmiColors[i].rgbRed = i;
m_pBitmapInfo->bmiColors[i].rgbGreen = i;
m_pBitmapInfo->bmiColors[i].rgbBlue = i;
}
m_pBitmapInfo->bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
m_pBitmapInfo->bmiHeader.biBitCount = (model == false) ? 8 : 24;
m_pBitmapInfo->bmiHeader.biPlanes = 1;
m_pBitmapInfo->bmiHeader.biCompression = 0;
m_pBitmapInfo->bmiHeader.biXPelsPerMeter = 0;
m_pBitmapInfo->bmiHeader.biYPelsPerMeter = 0;
m_pBitmapInfo->bmiHeader.biClrImportant = 0;
m_pBitmapInfo->bmiHeader.biClrUsed = 0;
m_pBitmapInfo->bmiHeader.biHeight = tSizeImg.cy;
m_pBitmapInfo->bmiHeader.biWidth = tSizeImg.cx;
int bitCount = (model == false) ? 8 : 24;
m_iImageWidthStep = ((tSizeImg.cx * bitCount / 8) + 3) & ~3;
m_pBitmapInfo->bmiHeader.biSizeImage = m_iImageWidthStep * tSizeImg.cy;
m_ImageDC.CreateCompatibleDC(pDC);
m_ImageDC.SetStretchBltMode(HALFTONE); //( COLORONCOLOR ); //(HALFTONE);
m_ImageDC.SetBkMode(TRANSPARENT);
if (m_hImageBitmap)
{
DeleteObject(m_hImageBitmap);
m_hImageBitmap = NULL;
}
m_hImageBitmap = CreateDIBSection(m_ImageDC.m_hDC, m_pBitmapInfo, DIB_RGB_COLORS, &m_pBuffdata, NULL, 0);
m_iImageSize = m_iImageWidthStep * tSizeImg.cy * sizeof(unsigned char);
memset(m_pBuffdata, 0, m_iImageSize);
m_ImageDC.SelectObject(m_hImageBitmap);
}
//二级缓存初始化
m_cBitmap.CreateCompatibleBitmap(pDC, m_tSizeWnd.cx, m_tSizeWnd.cy);
m_buffDC_image.CreateCompatibleDC(pDC);
m_buffDC_image.SetStretchBltMode(HALFTONE); //( COLORONCOLOR ); //(HALFTONE);
m_buffDC_image.SetBkMode(TRANSPARENT);
m_buffDC_image.SelectObject(&m_cBitmap);
注:需要重写这两个函数
virtual void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct);
//这个函数中 设置格式SS_OWNERDRAW 之后 ,调用Invalidate(false)
//才会去执行DrawItem()函数
virtual void PreSubclassWindow()
{
ModifyStyle(SS_TYPEMASK,
SS_OWNERDRAW | SS_NOTIFY,
SWP_FRAMECHANGED);
CStatic::PreSubclassWindow();
}