• GDI+学习笔记(九)带插件的排序算法演示器(MFC中的GDI+实例)


    带插件的排序算法演示器

    请尊重本人的工作成果,转载请留言。并说明转载地址,谢谢。

    地址例如以下:

    http://blog.csdn.net/fukainankai/article/details/27710883


    本节将通过一个实例来说明GDI+在MFC中的应用。这个算法演示器事实上是本人算法系列的一个开端,因为csdn没有树状的文件夹结构,咱也仅仅好使用链表了不是?好了。废话不多说,開始今天的文章。


    (一)功能说明

    我们初步制定功能例如以下:

    (1). 可以通过柱状图。自己主动展示排序算法的交换比較过程

    (2). 可以使用插件的形式进行开发。即,当新完毕一个算法后。仅仅须要完毕一个插件文件(我们这里使用动态库dll),由主程序载入插件。就可以进行运行,而不再须要又一次编译主程序。

    (3). 保证主程序的独立性。

    即,进行主程序的时候,插件格式不变。

    (4). 能够设置排序的规模。即,排序的数目。

    (5). 能够暂停演示,并手动逐步进行上一步或下一步操作。


    (二)插件原理

    我们的插件採用了动态库。尽管对于载入动态库的方法,网上非常多大牛都罗列了一二三,但个人认为。假设你拥有一些简单的汇编知识就会发现,事实上不管你是通过load静态库,包括头文件还是什么其它的方式载入动态库,事实上原理都是一样的。

    (1). 向编译器解释你的动态库

    我们要告诉编译器,一些动态库的信息。这些动态库的信息应该包括:

    1. 输出函数的调用约定。

    约定的一般类型有下面几种:

    __stdcall,__cdecl,__fastcall,__thiscall,__nakedcall,__pascal

    当中除了最后一种_pascal之外。其它同样,它们与_pascal的差别在于:

    a. 前者的參数顺序是,从右到左依次入栈(个人更喜欢觉得pop的顺序从左到右,个人理解,如有偏差,请留言,谢谢。)后者相反。

    b. 前者是调用者清除栈,后者是调用者返回后清除栈。


    2. 函数地址

    使用一个函数。我们还须要知道函数的地址。


    3. 函数原型


    有了以上三个部分,我们就能够成功的完毕对动态库的调用。

    (2)插件的实现

    1. 返回一个算法实例

    我们这里调用动态库的目的,是为了返回一个算法类实例的指针,动态库唯一的对外接口就是为了实现这个目的,以下是接口的实现代码:

    BOOL WINAPI Plug_CreateObject(void ** pobj)
    {
    	*pobj = new CAlgorithmCls;
    	return *pobj != NULL;
    }
    
    动态库只须要输出上面的一个函数,当中WINAPI是告诉编译这个函数的调用约定,这个宏与_stdcall是一样的。

    2. 主程中使用接口

    对于每种算法,我们都须要通过这个接口获得我们所须要的类实例指针。

    在这之前,我们得载入动态库。载入方法例如以下:

    	PLUG_ST stPs;
    	ZeroMemory(&stPs, sizeof(stPs));
    	stPs.hIns = LoadLibrary(strPlugPath);
    	PFN_Plug_CreateObject pFunc = (PFN_Plug_CreateObject)GetProcAddress(stPs.hIns, "Plug_CreateObject");
    
    这里有点陌生的东西,就是结构体PLUG_ST,这个结构的定义例如以下:

    typedef struct{
    	CPlugBase * pObj;
    	HINSTANCE hIns;
    }PLUG_ST, * LPPLUG_ST;
    看名字,相信聪明的朋友已经猜出一二。它两个成员,pObj是返回的算法类的基类指针。稍后我们会做具体介绍;还有一个是动态库的模块句柄。也就是LoadLibrary的返回值,我们能够觉得,它就是动态库的一个身份证。


    假设成功载入了。那么,我们就能够通过通过调用它的唯一接口,来获得结构体中的第一个成员。即算法实例指针。代码例如以下:

    	if (pFunc!=NULL && pFunc((void **)&stPs.pObj))
    	{
    		m_vecPlugs.push_back(stPs);
    
    		m_comboAlg.InsertString(0, strAlgName);
    	}
    乍一看。似乎有点复杂。事实上分开了慢慢看,事实上非常easy。

    首先。我们使用一个vector存储了全部的结构体。括号内的第二句话,仅仅是向combo控件中插入了一个算法的名字

    然后。条件中第一部分,要求pFunc非Null,pFunc是GetProcAddress的返回值,假设返回了空,那也就是说。我们没有成功的找到输出函数Plug_CreateObject,那么自然没法进行下一步运算。

    最后,终于的就是调用我们刚才获得的函数指针pFunc,来对输出參数赋值。以得到算法的实例指针。

    至此,我们就完毕了插件的核心代码


    3. 插件Base类

    我们通过一个虚的插件Base类。来要求插件实现者。必须完毕的插件功能。

    这个虚类的所有代码例如以下:

    class CPlugBase 
    {
    public:
    	CPlugBase(){};
    	virtual ~CPlugBase(){;}
    
    	virtual void SetData(int nCount, int *pData) = 0;
    	virtual void Start() = 0;
    	virtual bool GetNextOp(int &x, int &y, int &op) = 0;
    	virtual bool GetLastOp(int &x, int &y, int &op) = 0;
    	virtual void End() = 0;
    };

    我们简单说明一下,几个函数的功能。

    SetData,主程序通过这个函数。向插件实现者提供排序的容量和排序的数据。

    Start,主程序通过该函数。要求插件实现者。进行他自己的排序运算。

    当然,这个函数必须在设置完成数据之后。才可以进行。

    GetNextOp/GetLastOp,主程序通过这两个函数,向插件索取一次操作的内容,x,y各自是须要进行操作的两个数据的索引,op为操作类型,我们临时将其定义为枚举。例如以下:

    	enum AlgOp{
    	ALG_SWAP = 0,
    	ALG_COMPARE,
    };

    End,主程序通过这个函数。告知插件,能够进行清除工作。


    我们每一次操作的步骤。会记录在一个结构体中,它包括了刚才參数中的三个值,插件实现者能够利用该结构。也能够自己写,甚至不用结构。

    下面是结构体的实现:

    #pragma pack(push, 4)
    struct AlgStep{
    	AlgOp op;
    	int nFirstIndex;	// 前者的交换索引
    	int nNextIndex;		// 后者的交换索引
    
    
    	AlgStep &operator = (const AlgStep &_algStep){
    		op = _algStep.op;
    		nFirstIndex = _algStep.nFirstIndex;
    		nNextIndex = _algStep.nNextIndex;
    
    		return *this;
    	}
    };
    #pragma pack(pop)

    这个结构体处,有两点很重要。

    a. 字节对齐。

    起初。我索引值获取的时候总是错的。调试之后也发现问题,改动对齐方式后,就正确了,当时没有细致考虑。

    后来分析了一下,这里对齐方式并不会有太大影响。造成问题的解决办法可能是,之前的某次改动并没有进行又一次编译,使用了旧的obj对象进行链接。导致实际运行的代码,和调试代码不匹配,所以才出现故障的。

    改动对齐方式后。进行了又一次编译,所以没问题。

    b. “=”的重载非常必要。这涉及到深拷贝和浅拷贝的问题。

    说起来好像非常专业,事实上非常easy,就是默认的拷贝函数,仅仅会拷贝结构体实例的地址,而不会结构体实例中详细的各个值传进去,所以有必要重载这个函数。


    这样就完毕了插件的基类,这个基类的作用是连接主程序与插件子类,插件实现者,通过重载该基类。实现相应的虚函数,就能够在这个算法演示器上。演示自己的排序算法。


    4. 简单实现一个不是排序的排序算法

    首先。须要新建一个dll库的项目,并将base.h包括进去,当然也能够自己重写一份,重写的时候仅仅须要保证自己写的Base和原来的CPlugBase类的形式一模一样即可。即:类的函数成员的形式同样。这里,我们为了与主程序一致,将CPlugBase加入到了自己定义的Include路径中。

    然后,继承CPlugBase。实现各个纯虚函数。

    (1)SetData。将排序规模和排序数据存入当前的数据成员中

    void CAlgorithmCls::SetData( int nCount, int *pData )
    {
    	m_nCount = nCount;
    	m_pData = pData;
    }
    (2)Start,存入一些简单数据操作。我们这里不过做一些比較。假设是完整的排序算法的话,须要完好该函数,代码例如以下:

    void CAlgorithmCls::Start()
    {
    	for(int i=0; i<m_nCount-1; i++)
    	{
    		AlgStep as;
    		as.nFirstIndex = i;
    		as.nNextIndex = i+1;
    		as.op = ALG_COMPARE;
    
    		m_algSteps.push_back(as);
    	}
    	m_nCurStep = 0;
    }
    

    这里m_nCurStep,表示当前运行的步骤,每次计算的时候,我们须要将当前步骤清空至0.

    (3)GetNextOp/GetLastOp,这两个函数,分别用于获得下一步/上一步操作内容,同一时候在该函数内运行交换操作。这里我们临时没有写交换操作,可是数据的指针和交换数据的索引都知道。这不该是什么难事吧,代码例如以下:

    bool CAlgorithmCls::GetNextOp( int &x, int &y, int &op )
    {
    	if (m_nCurStep++ < m_algSteps.size()-1)
    	{
    		x = m_algSteps[m_nCurStep].nFirstIndex;
    		y = m_algSteps[m_nCurStep].nNextIndex;
    		op = (int)m_algSteps[m_nCurStep].op;
    
    		if (m_algSteps[m_nCurStep].op == ALG_SWAP)
    		{
    			// ...
    		}
    	}
    
    	int n = sizeof(AlgOp);
    
    	if (m_nCurStep >= m_algSteps.size())
    	{
    		return false;
    	}
    
    	return true;
    }

    假设返回了true。说明尚未完毕排序,返回false。说明已经完毕了全部排序的操作。

    (4)End。这里我们临时没有什么须要清理的数据,由于在类内我们没有分配数据。依照谁分配谁释放的原则。pData指针也交给外部去释放。

    最后,将输出文件名称改成算法名,我们这里改成了冒泡排序.dll

    5. GDI+完毕绘制

    (1)GDI+的初始化。好像已经说了非常多遍了,再反复一次吧,包括头文件<objbase.h>和头文件<gdiplus.h>,使用GdiPlus的名字控件。使用“#pragma comment(lib, "gdiplus.lib")”来完毕静态库的载入。

    GDI+的初始化。OnInitDialog时,使用GdiplusStartup初始化GDI+,OnDestroy时,释放GDI+资源。OnPaint时,进行绘制。

    (2)演示程序的绘制。先看代码再解释。

    		CDC *pCDC = GetDlgItem(IDC_SHOWPIC)->GetDC();
    		HDC hdc = pCDC->GetSafeHdc();
    		Graphics grphics(hdc);
    
    		RECT rect;
    		GetDlgItem(IDC_SHOWPIC)->GetClientRect(&rect);
    
    		Bitmap bitmap(rect.right-rect.left, rect.bottom-rect.top);
    		Graphics grp(&bitmap);
    		grp.Clear(Color::Black);
    
    		int nWidth = (rect.right-rect.left)/m_nCount/2;
    		int nOffset = (rect.right-rect.left-nWidth*2*m_nCount)/2;	// 整数运算造成的偏移
    		int nBottom = 20;
    
    		for(int i=0; i<m_nCount; i++)
    		{
    			RECT box;
    			int nHeight = (rect.bottom-rect.top-nBottom)/m_nCount*(*(m_pData+i));
    			box.left = nWidth + i*nWidth*2 + nOffset;
    			box.right = box.left+nWidth;
    			box.bottom = rect.bottom-nBottom;
    			box.top = box.bottom - nHeight;
    
    			if (i!= m_CurOp.nFirstIndex && i!= m_CurOp.nNextIndex)
    			{
    				SolidBrush sbrush(Color::Crimson);
    				grp.FillRectangle(&sbrush, box.left, box.top, box.right-box.left,
    					nHeight);
    			}
    			else
    			{
    				SolidBrush sbrush(m_Specialcolor);
    				grp.FillRectangle(&sbrush, box.left, box.top, box.right-box.left,
    					nHeight);
    			}
    		}
    		
    		grphics.DrawImage(&bitmap, 0, 0);

    我们先在一张Bitmap中。进行绘制,然后将Bitmap的内容绘制到图形控件上(事实上就是一个矩形的Static控件。本质仅仅要是一个CWnd都能够进行绘制)。

    由于我们从头到尾坐标使用的都是整数,一个柱形有一点点误差。没有问题,可是多了的话偏移就会非常大,我们将全部的偏移计算一下。平均分配到两边,就不至于让终于的图像太靠向一边了。

    柱形的size计算非常easy。可是非常罗嗦。大家看看就好。我就不做过多解释了。

    每一个柱形矩形的绘制,须要填充一个实画刷。然后用该画刷填充一个矩形,就完毕了终于的绘制。

    背景颜色,填充成了黑色。

    至此。我们完毕了最初图形的绘制,简单看看效果。



    6. 手动运行上一步或者下一步

    每次绘制的时候。我们须要记录当前的操作是什么。当前操作所相应的颜色是什么,获得上一步。下一步都须要完毕这种工作,先看一下下一步的代码:

    void CAlgorithmDemoDlg::OnNextStep()
    {
    	m_Specialcolor = Color::Blue;
    
    	int nOp;
    
    	m_vecPlugs[m_nCurAlg].pObj->GetNextOp(m_CurOp.nFirstIndex, m_CurOp.nNextIndex,
    		nOp);
    
    	m_CurOp.op = (AlgOp)nOp;
    
    	Invalidate(FALSE);
    }
    

    这里我们事实上应该依据操作类型nOp来决定特殊的颜色,这里为了省事,我们直接定义成了蓝色。另外,我们的绘制是放在OnPaint函数中的,所以。在获得下一步的时候。我们应该告诉程序,该刷新图形框了。最直白的方法就是向程序发送一个OnPaint消息。这里我们使用Invalidate,这个函数会发送OnPaint函数,FALSE表示不擦除,假设TRUE的话。OnPaint就什么都不绘制了。

    当然我们也能够使用基类的Invalidate来重绘一个区域。

    ::InvalidateRect(m_hWnd, NULL, bErase);

    我们眼下相当于调用了上述代码,假设要重绘一个矩形。仅仅须要将NULL替换成你须要重绘的RECT实例的指针就可以。


    7. 定时器与自己主动演示

    MFC中的定时器非常easy,我们仅仅须要在開始自己主动演示时,设置一个定时器,演示完毕后。Kill掉这个定时器,然后完毕OnTimer的消息响应就能够了。

    自己主动演示消息

    void CAlgorithmDemoDlg::OnBnClickedAutorun()
    {
    	SetTimer(1, 500, NULL);
    }

    OnTimer完毕定时器的响应

    void CAlgorithmDemoDlg::OnTimer(UINT_PTR nIDEvent)
    {
    	// TODO: 在此加入消息处理程序代码和/或调用默认值
    	int nOp;
    	bool bRet;
    	switch(nIDEvent)
    	{
    	case 1:
    		bRet = m_vecPlugs[m_nCurAlg].pObj->GetNextOp(m_CurOp.nFirstIndex, m_CurOp.nNextIndex,
    			nOp);
    		m_CurOp.op = (AlgOp)nOp;
    		if (!bRet)
    		{
    			KillTimer(1);<span style="white-space:pre">	</span>// 算法演示完成或失败,则停止自己主动演示
    		}
    		Invalidate(FALSE);
    		break;
    	}
    
    	CDialog::OnTimer(nIDEvent);
    }
    

    看下效果:


    至此,演示程序,基本完毕。还有些细节,在这里就不再多说了。另外,gif的生成难度要高过我的预估。c/c++中gdi+不兼容动态的gif,看来仅仅能依据gif格式进行手工编码合并了。


    假设大家发现有什么不正确的地方,请及时提出,谢谢!~


    之后就能够開始排序算法的历程啦。只是GDI+系列还没有结束,另一点点的细节会慢慢添加。另外,欢迎对C/C++语言开发的爱好者加群一起讨论关于C/C++语言的各种技术!~群号:69788620,群内有各类开发相关的资料可供下载,






  • 相关阅读:
    linux学习之linux的hostname修改详解《转》
    不想作死系列--win7远程linux桌面之vncserver
    不想作死系列---virtualbox最小化安装centos6.5
    基于支持向量机的车牌识别-- opencv2.4.7+vs2012环境搭建
    python文件的中文处理以及个人思路
    haskell学习笔记<1>--基本语法
    提醒
    C语言矩阵传递给函数的方法
    0x01数据结构——C语言实现(二叉查找树)
    0x01数据结构——C语言实现(二叉树)
  • 原文地址:https://www.cnblogs.com/gavanwanggw/p/6734256.html
Copyright © 2020-2023  润新知