一、前言
前面几回已经实现了动态绘制图形,并且使用内存DC保存绘图数据,但是并没有实现落盘,关掉窗口数据就丢失了,现在这回需要能够将内存DC中的数据保存到文件中,并且同时支持打开对应文件,我们将使用到CImage库加速实现。
二、实现步骤
1.在Doc类增加OnSaveDocument方法
2.在Doc类增加OnOpenDocument方法
3.将View类的DC数据通过CImage保存
4.通过CImag对象加载文件数据到View类的DC数据
三、实现代码
1.实现OnSaveDocument方法
头文件中增加成员变量和成员函数
public:
virtual BOOL OnOpenDocument(LPCTSTR lpszPathName);
virtual BOOL OnSaveDocument(LPCTSTR lpszPathName);
// 通过内存DC的数据构造CImage对象
bool CreateCImageFromMemDC(CDC& memDC, CImage& outImage);
CDC* GetMemDC();
CBitmap* GetMemBitMap();
// 初始化m_memDC
void InitMemDC(int width, int height);
// 标记文件数据已加载,需要拷贝到View类中
bool isOpenFile;
private:
// Doc类中的内存DC,用来暂存数据
CDC m_memDC;
CBitmap m_memBitmap;
初始化内存DC数据
CDC* CMFCTest2Doc::GetMemDC()
{
return &m_memDC;
}
CBitmap* CMFCTest2Doc::GetMemBitMap()
{
return &m_memBitmap;
}
void CMFCTest2Doc::InitMemDC(int width, int height)
{
if (m_memDC.GetSafeHdc())
m_memDC.DeleteDC();
if (m_memBitmap.GetSafeHandle())
m_memBitmap.DeleteObject();
CClientDC dc(AfxGetMainWnd());
m_memDC.CreateCompatibleDC(&dc);
m_memBitmap.CreateCompatibleBitmap(&dc, width, height);
m_memDC.SelectObject(&m_memBitmap);
// 用白色填充
m_memDC.FillSolidRect(0, 0, width, height, RGB(255, 255, 255));
}
通过内存DC数据构造CImage对象的函数
bool CMFCTest2Doc::CreateCImageFromMemDC(CDC& memDC, CImage& outImage)
{
// 1. 验证输入DC
if (!memDC.GetSafeHdc()) {
TRACE(_T("无效的内存DC句柄\n"));
return false;
}
// 2. 获取当前位图
CBitmap* pBitmap = memDC.GetCurrentBitmap();
if (!pBitmap || !pBitmap->GetSafeHandle()) {
TRACE(_T("内存DC未选中位图\n"));
return false;
}
// 3. 获取位图信息
BITMAP bmpInfo;
if (!pBitmap->GetBitmap(&bmpInfo)) {
TRACE(_T("获取位图信息失败\n"));
return false;
}
// 4. 创建CImage
if (!outImage.Create(bmpInfo.bmWidth, bmpInfo.bmHeight, bmpInfo.bmBitsPixel)) {
TRACE(_T("创建CImage失败 (宽=%d, 高=%d, 位深=%d)\n"),
bmpInfo.bmWidth, bmpInfo.bmHeight, bmpInfo.bmBitsPixel);
return false;
}
// 5. 复制数据
HDC hImageDC = outImage.GetDC();
if (!hImageDC) {
TRACE(_T("获取CImage DC失败\n"));
return false;
}
if (!::BitBlt(hImageDC, 0, 0, bmpInfo.bmWidth, bmpInfo.bmHeight,
memDC.GetSafeHdc(), 0, 0, SRCCOPY)) {
TRACE(_T("BitBlt失败 (错误码:%d)\n"), GetLastError());
outImage.ReleaseDC();
return false;
}
outImage.ReleaseDC();
return true;
}
获取View类的DC数据,拷贝到Doc类的DC中,然后构造CImage对象,保存到文件中
BOOL CMFCTest2Doc::OnSaveDocument(LPCTSTR lpszPathName)
{
// 检查路径是否有效
if (lpszPathName == NULL || lpszPathName[0] == _T('\0'))
{
AfxMessageBox(_T("文件路径为空"));
return FALSE;
}
// 获取View类的指针,用来访问View类的方法
POSITION pos = GetFirstViewPosition();
CMFCTest2View* pView = (CMFCTest2View*)GetNextView(pos);
// 将内存DC内容保存到CImage
CImage tempImage;
int screenWidth = GetSystemMetrics(SM_CXSCREEN); // 获取主显示器宽度
int screenHeight = GetSystemMetrics(SM_CYSCREEN); // 获取主显示器高度
// 初始化m_memDC
InitMemDC(screenWidth, screenHeight);
// 将View类的DC数据拷贝到m_memDC中
m_memDC.BitBlt(0, 0, screenWidth, screenHeight,
pView->GetMemDC(), 0, 0, SRCCOPY);
// 通过拷贝后的DC构造CImage对象
CreateCImageFromMemDC(m_memDC, tempImage);
// 保存图像,调用CImage封装的方法
HRESULT hr = tempImage.Save(lpszPathName, Gdiplus::ImageFormatBMP);
if (FAILED(hr))
{
CString strError;
strError.Format(_T("保存图像失败 (错误码: 0x%08X)\n"),
hr);
AfxMessageBox(strError);
return FALSE;
}
SetModifiedFlag(FALSE); // 清除修改标志
return TRUE;
}
这里有一个题外话,就是上面最后一句代码【return TRUE;】,一开始我没有改自动生成的【return CDocument::OnSaveDocument(lpszPathName)】,所以每次保存文件执行到函数最后都会去调用一下基类的函数,最后导致没有返回TRUE,一直保存不成功,被折磨了挺久没发现。
2.实现OnOpenDocument方法
获取View类的指针,通过CImage对象加载文件数据,然后将数据保存到Doc类的DC中
BOOL CMFCTest2Doc::OnOpenDocument(LPCTSTR lpszPathName)
{
// 获取View类的指针,用来访问View类的方法
POSITION pos = GetFirstViewPosition();
CMFCTest2View* pView = (CMFCTest2View*)GetNextView(pos);
if (!pView)
{
return false;
}
if (!CDocument::OnOpenDocument(lpszPathName))
return FALSE;
int screenWidth = GetSystemMetrics(SM_CXSCREEN); // 获取主显示器宽度
int screenHeight = GetSystemMetrics(SM_CYSCREEN); // 获取主显示器高度
CImage tempImage;
// 从文件加载数据构造CImage对象
HRESULT hr = tempImage.Load(lpszPathName);
if (FAILED(hr))
{
CString strError;
strError.Format(_T("无法加载图像文件 (错误码: 0x%08X)\n"),
hr);
AfxMessageBox(strError);
return FALSE;
}
// 初始化m_memDC
InitMemDC(screenWidth, screenHeight);
// 将CImage的加载的数据拷贝到Doc类的m_memDC中
m_memDC.BitBlt(0, 0, screenWidth, screenHeight,
CDC::FromHandle(tempImage.GetDC()), 0, 0, SRCCOPY);
// 上面GetDC(),这里要Release
tempImage.ReleaseDC();
// 标记文件数据已加载完成
isOpenFile = true;
return TRUE;
}
在View类的Draw方法将Doc类的DC数据拷贝到View自己的DC中,然后复制到屏幕上
void CMFCTest2View::OnDraw(CDC* pDC)
{
CMFCTest2Doc* pDoc = GetDocument();
CDC* pMemDC = pDoc->GetMemDC();
int screenWidth = GetSystemMetrics(SM_CXSCREEN); // 获取主显示器宽度
int screenHeight = GetSystemMetrics(SM_CYSCREEN); // 获取主显示器高度
if (pDoc->isOpenFile)
{
m_memDC.BitBlt(0, 0, screenWidth, screenHeight,
pMemDC, 0, 0, SRCCOPY);
pDoc->isOpenFile = false;
}
ASSERT_VALID(pDoc);
if (!pDoc)
return;
CRect rect;
GetClientRect(&rect);
pDC->BitBlt(0, 0, rect.Width(), rect.Height(), &m_memDC, 0, 0, SRCCOPY);
// TODO: 在此处为本机数据添加绘制代码
}
四、代码改进
从上面的代码实现可以看出来其实简单的几步操作实现起来代码却很多,现在这一小节尝试改进。
1.OnSaveDocument方法改进
该方法只需要保证View类中的DC数据保存到文件即可,因此不需要先拷贝到Doc类的DC中,直接使用View类的DC数据创建CImage对象,然后保存到文件
BOOL CMFCTest2Doc::OnSaveDocument(LPCTSTR lpszPathName)
{
// 检查路径是否有效
if (lpszPathName == NULL || lpszPathName[0] == _T('\0'))
{
AfxMessageBox(_T("文件路径为空"));
return FALSE;
}
// 获取View类的指针,用来访问View类的方法
POSITION pos = GetFirstViewPosition();
CMFCTest2View* pView = (CMFCTest2View*)GetNextView(pos);
// 将内存DC内容保存到CImage
CImage tempImage;
// 通过拷贝后的DC构造CImage对象
CreateCImageFromMemDC(pView->GetMemDC(), tempImage);
// 保存图像,调用CImage封装的方法
HRESULT hr = tempImage.Save(lpszPathName, Gdiplus::ImageFormatBMP);
if (FAILED(hr))
{
CString strError;
strError.Format(_T("保存图像失败 (错误码: 0x%08X)\n"),
hr);
AfxMessageBox(strError);
return FALSE;
}
SetModifiedFlag(FALSE); // 清除修改标志
return TRUE;
}
2.OnOpenDocument方法改进
OnOpenDocument方法也可以简化,只需要提供一个m_tempImage成员变量保存加载进来的CImage对象即可,然后标记文件已打开
public:
virtual BOOL OnOpenDocument(LPCTSTR lpszPathName);
virtual BOOL OnSaveDocument(LPCTSTR lpszPathName);
// 通过内存DC的数据构造CImage对象
bool CreateCImageFromMemDC(CDC* memDC, CImage& outImage);
// 标记文件数据已加载,需要拷贝到View类中
bool isOpenFile;
public:
// 用于存放加载进来的文件数据
CImage m_tempImage;
BOOL CMFCTest2Doc::OnOpenDocument(LPCTSTR lpszPathName)
{
// 获取View类的指针,用来访问View类的方法
POSITION pos = GetFirstViewPosition();
CMFCTest2View* pView = (CMFCTest2View*)GetNextView(pos);
if (!pView)
{
return false;
}
if (!CDocument::OnOpenDocument(lpszPathName))
return FALSE;
// 从文件加载数据构造CImage对象
HRESULT hr = m_tempImage.Load(lpszPathName);
if (FAILED(hr))
{
CString strError;
strError.Format(_T("无法加载图像文件 (错误码: 0x%08X)\n"),
hr);
AfxMessageBox(strError);
return FALSE;
}
isOpenFile = true;
return TRUE;
}
其次就是在View类中获取Doc类的指针,然后获取其中的m_tempImage成员变量,再拷贝到View类的m_memDC即可。
// 初始化
void CMFCTest2View::OnInitialUpdate()
{
CView::OnInitialUpdate();
int screenWidth = GetSystemMetrics(SM_CXSCREEN); // 获取主显示器宽度
int screenHeight = GetSystemMetrics(SM_CYSCREEN); // 获取主显示器高度
InitMemDC(screenWidth, screenHeight);
CMFCTest2Doc* pDoc = GetDocument();
if (pDoc->isOpenFile)
{
SetMemDC();
pDoc->isOpenFile = false;
}
// TODO: 在此添加专用代码和/或调用基类
}
void CMFCTest2View::SetMemDC() {
CMFCTest2Doc* pDoc = GetDocument();
if (pDoc->m_tempImage.IsNull())
{
return;
}
int screenWidth = GetSystemMetrics(SM_CXSCREEN); // 获取主显示器宽度
int screenHeight = GetSystemMetrics(SM_CYSCREEN); // 获取主显示器高度
m_memDC.BitBlt(0, 0, screenWidth, screenHeight,
CDC::FromHandle(pDoc->m_tempImage.GetDC()), 0, 0, SRCCOPY);
// 上面GetDC(),这里要Release
pDoc->m_tempImage.ReleaseDC();
}
3.注意点
这里为什么是在View类的OnInitialUpdate()函数里拷贝呢,可不可以在OnOpenDocument方法里直接调用SetMemDC()函数呢?就像下面这样
BOOL CMFCTest2Doc::OnOpenDocument(LPCTSTR lpszPathName)
{
// 获取View类的指针,用来访问View类的方法
POSITION pos = GetFirstViewPosition();
CMFCTest2View* pView = (CMFCTest2View*)GetNextView(pos);
if (!pView)
{
return false;
}
if (!CDocument::OnOpenDocument(lpszPathName))
return FALSE;
// 从文件加载数据构造CImage对象
HRESULT hr = m_tempImage.Load(lpszPathName);
if (FAILED(hr))
{
CString strError;
strError.Format(_T("无法加载图像文件 (错误码: 0x%08X)\n"),
hr);
AfxMessageBox(strError);
return FALSE;
}
// 打开文件后立即刷新View类的DC数据
pView->SetMemDC();
isOpenFile = true;
return TRUE;
}
事实是这样不可以,因为执行完OnOpenDocument()方法后,依次会执行View类的OnInitialUpdate() -> OnDraw()方法,而在OnInitialUpdate()方法中对m_memDC做了初始化动作,因此在OnOpenDocument()方法中刷新的DC数据并不会生效。
五、总结
最终实现了保存和打开文件的功能,但是看起来并不是非常完美,后续可以看情况优化,具体代码可以参考下面链接。