转载请注明出处,相关程序附在博文最下边。欢迎留言交流,共同进步!
在MFC中直接制作一个好看的界面不是特别方便,而且复用性不是那么好。偶然看到网上界面库,想到要是将资源全部封装在dll里面,在一个新的工程中直接使用那该多方便!所以就尝试将资源等封装在dll里面,方便其他程序的调用。先看看封装在dll中的界面容貌。整个看起来还是比较好看的。
想要做出好看的界面,好看的界面元素是必不可少的,可以再网上找一些界面元素,然后对界面元素进行修改(PhotoShop是个可以很好使用的工具)。
上图界面上有一个大的背景图片,右上角的三个按钮(每个按钮有两个状态,鼠标在或不在按钮上,所以有6个小图片)。可以看到界面上右上角的三个图标和界面并没有不和谐的瑕疵出现,那是因为按钮的小图片是在原图上截取的。做好了界面元素就可以进行重要的步骤了。上述素材的修改都是在photoshop中完成的,修改图片真是一个细活!
1、创建MFC dll工程。因为我们要将界面对话框封装到dll中,而且界面上的元素都要包含到dll中,所以这里要选择使用静态的链接方式。静态的方式得到的dll就不依赖外部的资源了!那是因为资源都在dll里面了,所以静态链接的dll相对比较大。
2、向工程中添加一个CDialog类。当然这时候会有一个对话框资源出现。我们主要的编码工作就是要在这个类中完成。
3、在CDialog类中完成对对话框界面的绘制。具体方式就不用介绍了。界面贴图的方式后面分析,相关问题可以参看之前的博文(MFC实现五子棋),或者直接看本博文的源码。
*4、界面绘制完成了,就该思考另外一个问题了。怎么去访问dll中的这个类呢?好像没有什么很方便的方式来对它进行访问,那么不妨我们定义一个中间函数。这个函数来联系外部调用程序和dll中的对话框类。
在这里定义一个函数(这个函数在dll工程里全局区):
int _stdcall ShowDlg(CString& szName,CString& szPass) { CLoginDlg dlg; if (IDOK == dlg.DoModal()) { dlg.GetPassAndName(szName, szPass); } return dlg.m_State; }
上述函数的功能就是调用我们绘制对话框界面的CLoginDlg类来显示对话框。这里的参数先不用管,后面会介绍它们的作用。
上面的函数是在全局区里面的。我们想要在dll外部对其调用就必须要导出该函数。所以在dll工程中的.def文件添加该函数的“声明”:
; Skindll.def : 声明 DLL 的模块参数。 LIBRARY EXPORTS ShowDlg; 此处可以是显式导出
编译程序就可以得到dll了。为了方便外部函数对dll的调用在两者间在添加一个“桥梁”,这个桥梁的作用就是让外部程序直接调用这个“桥梁”中的函数就好了,不用去写导入dll这样显得比较臃肿的代码。
logindlgdll.h
#ifndef __LOGINDLGDLL_H__ #define __LOGINDLGDLL_H__ #include "stdafx.h" typedef int (_stdcall *funShowDlg)(CString &szName,CString& szPass); int ShowLoginDlg(CString &szName,CString& szPass); #endiflogindlgdll.cpp
#include "stdafx.h" #include "LoginDlgDll.h" int ShowLoginDlg(CString& szName,CString& szPass) { int state = -1; HMODULE hM = LoadLibrary("Skindll.dll"); funShowDlg Dlg; if(hM) { Dlg = (funShowDlg)GetProcAddress(hM, "ShowDlg"); if (Dlg) state = Dlg(szName,szPass); } FreeLibrary(hM); return state; }
1、在解决方案中添加一个MFC应用工程项目(对话框应用程序),这个项目的目的仅仅是为了测试我们封装的dll。在界面上添加一个按钮,添加按钮按下响应函数。
2、包含前面“桥梁”的头文件,在响应函数中直接调用“桥梁”中的函数
CString a,b; while (a != "admin" || b != "admin") { if (ShowLoginDlg(a,b) == 2) { break; } }
因为两个项目都是在一个解决方案中的,所以他们生成的文件也都在一个文件夹下!就像下图一样:
应用程序和dll在一个目录下就可以保证在运行程序时动态库就可以被连接到。如果上面步骤都正确的话,应该就得到文章上面的那个图示的界面了。下面来分析界面绘制相关的内容。
界面绘制最关键的就是贴图了,中间涉及windows消息的响应。从使用的角度来看,界面一旦出现在我们眼前的时候就应该是我们想要的样子,也就说我们就要在界面还没有出现在眼前的时候就要对界面进行绘制。
考虑到这些,我们在对话框程序的OnInitDialog()中加载资源位图,然后去刷新界面,在WM_PAINT消息中区绘制界面。注意OnInitDialog()运行的时候界面还没有显示!所以可以在这里做一些初始化的工作,比如加载资源等等。然后在WM_PAINT中贴图。
为什么不直接在OnInitDialog()中贴图而是在WM_PAINT消息下绘制?如果直接绘制,在我们刷新界面的时候界面上的图片就会消失。WM_PAINT消息就是为了这个设计的。windows在重新绘制界面的时候会发送一个WM_PAINT的消息,在这个消息响应中绘制界面就可以保证我们看到的界面在刷新的时候图形不会消失了!也就是说在其他响应函数中绘制界面,都是临时的,界面一旦刷新,内容就没有了。
在OnInitDialog()函数中我们还去记录下界面小元素(右上角的几个元素)的位置,免得用的时候总去计算。运用的时候计算这种方式一方面使得运行效率变低(显然计算次数变多了),另一方便,代码量也变大了,麻烦!记录到成员变量中虽然多占用了内存,但是这种方式使用起来相对方便舒服。
BOOL CLoginDlg::OnInitDialog() { CDialog::OnInitDialog(); m_BkDC.LoadBitmap(IDB_BKBMP); //加载背景位图 m_cloesDC.LoadBitmap(IDB_CLOSE); //加载关闭按钮图 m_minDC.LoadBitmap(IDB_MIN); //加载最小化按钮图 m_SetDC.LoadBitmap(IDB_SETTING); //加载设置按钮图 m_rtClose = CRect(m_BkDC.Width()-m_cloesDC.Width(),0,m_BkDC.Width(), m_cloesDC.Height()); m_rtMin = CRect(m_rtClose.left - m_minDC.Width(), 0, m_rtClose.left, m_minDC.Height()); m_rtSet = CRect(m_rtMin.left - m_SetDC.Width(), 0, m_rtMin.left, m_SetDC.Height()); GetDlgItem(IDOK)->GetWindowRect(&m_rtOK); ScreenToClient(&m_rtOK); GetDlgItem(IDCANCEL)->GetWindowRect(&m_rtCancel); ScreenToClient(&m_rtCancel); MoveWindow(0, 0, m_BkDC.Width(), m_BkDC.Height()); //设置窗口大小为背景位图大小 CenterWindow(); Invalidate(FALSE); //刷新界面 return TRUE; }
在OnPaint()函数中贴图:
void CLoginDlg::OnPaint() { CPaintDC dc(this); // device context for painting dc.BitBlt(0, 0, m_BkDC.Width(), m_BkDC.Height(), &m_BkDC, 0, 0, SRCCOPY); dc.BitBlt(m_BkDC.Width()-m_cloesDC.Width(), 0, m_cloesDC.Width(), m_cloesDC.Height(), &m_cloesDC, 0, 0, SRCCOPY); dc.BitBlt(m_BkDC.Width()-m_cloesDC.Width() - m_minDC.Width(), 0, m_minDC.Width(), m_minDC.Height(), &m_minDC, 0, 0, SRCCOPY); dc.BitBlt(m_BkDC.Width()-m_cloesDC.Width() - m_minDC.Width()-m_SetDC.Width(), 0, m_SetDC.Width(), m_SetDC.Height(), &m_SetDC, 0, 0, SRCCOPY); }
为了去掉windows自带的丑陋的变宽,在界面设计的时候对话框资源选择的是没有边框的对话框(Bord属性为None)。没有了系统边框就意味着我们要自己去做最小化,最大化,关闭这样的功能按钮。
和这相关的有这几个消息:
PostMessage(WM_SYSCOMMAND, SC_MAXIMIZE, 0); // 最大化
PostMessage(WM_SYSCOMMAND, SC_MINIMIZE, 0); //最小化
PostMessage(WM_SYSCOMMAND, SC_RESTORE, 0); // 窗口复原
关闭直接使用OnCancel函数就行了。
很多程序都是去了windows边框的,可以参照金山管家的界面。博主猜测他们也是用我们这样的方式进行绘制的。只是他们的素材要更绚丽一些。对比我们程序的右上角和它们的右上角,是不是有点像,哈哈!!
金山卫士的界面在我们鼠标移动到按钮上面的时候按钮会发生一些变化,这是个很好的提示。我们也要实现这个,首先添加一个WM_MOUSEMOVE(鼠标移动消息)的消息响应。然后在这个函数里面去判断我们的鼠标是不是在按钮上,在按钮上我们就将另外一块小的图形绘制在该块区域上。
void CLoginDlg::OnMouseMove(UINT nFlags, CPoint point) { CDC *pDC = GetDC(); CRBMemoryDC dc1, dc2, dc3; if (m_rtClose.PtInRect(point)) dc1.LoadBitmap(IDB_SELCLOSE); else dc1.LoadBitmap(IDB_CLOSE); if (m_rtMin.PtInRect(point)) dc2.LoadBitmap(IDB_SELMIN); else dc2.LoadBitmap(IDB_MIN); if (m_rtSet.PtInRect(point)) dc3.LoadBitmap(IDB_SELSETTING); else dc3.LoadBitmap(IDB_SETTING); pDC->BitBlt(m_BkDC.Width()-m_cloesDC.Width(), 0, dc1.Width(), dc1.Height(), &dc1, 0, 0, SRCCOPY); pDC->BitBlt(m_BkDC.Width()-m_cloesDC.Width() - m_minDC.Width(), 0, dc2.Width(), dc2.Height(), &dc2, 0, 0, SRCCOPY); pDC->BitBlt(m_BkDC.Width()-m_cloesDC.Width() - m_minDC.Width()-m_SetDC.Width(), 0 , dc3.Width(), dc3.Height(), &dc3, 0, 0, SRCCOPY); ReleaseDC(pDC); CDialog::OnMouseMove(nFlags, point); }同理,我们在按钮上按下的时候也要有响应啊!所以还要添加一个WM_LBUTTONDOWN消息。和上述实现基本一致,所以就不用贴代码了,具体可以看源码。
在最前面的代码里面我们看到了调用的时候有很多参数。这是用做什么?
考虑我们登录QQ时候的动作,弹出界面,我们输入账户和密码,然后确认由服务器去判断我们的输入是不是正确的。我们的登录界面是封装在dll中的,判断账户密码的规则写在dll中显然是不合适的,因为我们的dll是要用到各种不同的场合的!所以我们要将我们输入的信息传出来给我们的应用程序来作判断处理,dll仅仅封装一个美丽的界面而已。当然,为了判断我们到底在登录的时候是按了登录按钮还是按下了取消按钮,我们的应用程序也是要知道的,所以传出一个参数用作判断我们的动作类型。
将生成的dll和 “桥梁” 函数文件LoginDlgDll.cpp和LoginDlgDll.h分别拷贝到exe路径下 和 工程源码下,将两个函数文件添加到项目中。以单文档项目中为例,包含头文件并在CXXXApp::InitInstance()中调用。如下图:
编译并运行程序,就会看到我们封装在dll中的漂亮登录界面先出现了,在我们验证不对的时候还一直出现。这就说明该dll可以在各个地方复用了。
这里还有很重要的一点就是在dll中使用了类,外部间接使用了类,这个想法值得在以后项目中使用。
点击源码下载