MFC图形编辑器
前言
vs2015竟然可以完美打开工程,哈哈可以直接生成类图了。由于内容较多,所以根据内容的重要性会安排详略。
https://github.com/bajdcc/GraphEditor/releases/tag/1.0
主要的内容:
- MFC的基本使用介绍
- 4种图形的绘制
- 图形的事件处理
- 撤销与恢复功能的实现
- 其他功能
介绍
MFC好歹是必学课目,其实搞GUI有多种方法,可以用Qt、WPF、SWT、Electron等等,之所以要学MFC是因为C++,还因为vc6.0体积小安装快,不需要安装其他重量级的库。
那么最基础的部分都不废话了。图形编辑器肯定要有保存功能、同时编辑多个图像、各种工具栏,所以要建立多文档的工程。看类图其实东西也不多,多了一些算法,哈这些算法比较有趣。那么本工程作为MFC的练习项目,需要读者先学习MFC相关的知识。
图形
图形的创建
这里只有四种图形:直线、矩形、椭圆、曲线(应该为折线),因为API支持这些多,其他图形太过复杂了。学习完多态就会知道,四种图形是继承自某一类的,这个基类就是CGraphic。
先来看看基类:
class CGraphic : public CObject
{
DECLARE_SERIAL( CGraphic )
public:
virtual void Serialize( CArchive &ar ); public:
CGraphic( UINT type = NONE );
virtual void UpdateData( GraphicMember* pSrc, BOOL bSave = TRUE ); virtual void Draw( CDC* pDC );
virtual void DrawSelectedEdge( CDC* pDC );
virtual void HitTest( CPoint& pt, BOOL& bResult );
virtual LPCTSTR HitSizingTest( CPoint& pt, BOOL& bResult, LONG** PtX = NULL, LONG** PtY = NULL );
virtual void GetRect( CRect& rt );
virtual LPCTSTR GetName() const;
virtual int GetPts() const;
virtual BOOL EnableBrush() const; public:
enum _GBS { GBS_PEN = 0x1, GBS_BRUSH }; static CGraphic* CreateGraphic( GraphicMember* );
static void GraphicDrawSelectedEdge( CDC* pDC, CPoint& pt, int& inflate );
static int GetIdBySelection( _GBS SelectType, int ID );
static int GetSelectionById( _GBS SelectType, int sel );
static LPCTSTR GetPenStyleById( int ID, BOOL bConvert = TRUE );
static LPCTSTR GetBrushStyleById( int ID, BOOL bConvert = TRUE );
static void CreateGdiObjectFromId( _GBS GdiType, int ID, CGdiObject* object, int width, int color );
static void GraphicHitSizingTest( LONG& x, LONG& y, int inf, CPoint& pt, BOOL& bResult,
LONG** X = NULL, LONG** Y = NULL ); protected:
static LONG DotsLengthSquare( CPoint& p1, CPoint& p2 );
static void LineHitTest( CPoint& p1, CPoint& p2, CPoint& p3, BOOL& bResult );
BOOL PtInRectTest( CPoint& pt ); public:
UINT m_DrawType;
BOOL m_bHidden;
CString m_lpszName;
CPoint m_pt1, m_pt2;
CTime m_createTime, m_modifiedTime;
};
除去一些MFC相关的方法,基类的内容很多,要实现图形的绘制、选中测试、序列化,以及Get/Set方法等。
来看看它的数据成员,包括了图形的类别、是否隐藏、自定义名称、起始点和终点、创建时间和修改时间。有人会说那折线是多个点的,两个点不肯存啊,不是的,这两个点是四种图形都会包括的,所以索性放基类中了。
工厂方法:
CGraphic* CGraphic::CreateGraphic( GraphicMember* pSrc )
{
ASSERT(pSrc);
CGraphic* pRet = NULL;
switch (pSrc->m_DrawType)
{
case LINE: pRet = new CLine; break;
case RECTANGLE: pRet = new CRectangle; break;
case ELLIPSE: pRet = new CEllipse; break;
case CURVE: pRet = new CCurve; break;
default: return NULL;
}
pRet->UpdateData(pSrc);
return pRet;
}
其实不复杂,就是根据名称创建相应对象而已。
图形的选中
鼠标可以选中图形并拖动它,改变它大小时,光标会变成相应的形态,这怎么实现呢?其实很多游戏都有选中图形如3D对象的功能,如MC、看门狗等,当然在2D世界中,问题相应简单的多,我们这里用最笨的方法,就是一个个找。。
在正式GUI中,控件间有父子和兄弟关系,这样的话,就是在一棵树中查找,效率相对高点,而本项目中所有图形是兄弟关系,所以只能一个个遍历啦~
那么线段的选中是怎样实现的?直线没有宽度啊。。这个问题也困扰了我,不过这里不要求精确,假设线段的两端点为AB,当然鼠标所在位置为C,只要算AC+BC跟AB很接近就可以了。
椭圆的选中呢?很简单,因为这里不支持旋转,所以椭圆是方正的,只要根据椭圆的二次解析式方程就可以判断,就点代进去,然后算大于0还是小于0。这里有个注意点:浮点数的大小判断不能用等号,要用不等式区间去判断。
折线的选中就是连着判断所有线段。
图形的调整与拖动
图形的调整大小:首先要选中图形,然后出现选中轮廓提示,再移动到轮廓上等光标改变,就可以改变图形的大小。这部分较简单。
图形的拖动:监听几个事件,OnLButtonDown/OnLButtonUp/OnMouseMove,如当前选中了哪个图形就要将它记录下来,万一要调整图形的大小了,就可以马上将记录下来的图形进行修改。这部分比较繁琐(代码比较乱),建议自己先建立Win32程序练习或参考更简单的代码。这部分就是个状态机,我也是debug了很久才把代码完善好的,这里也讲不明白。
图形的绘制
都是调的API:Ellipse/Rectangle/LineTo。
双缓存:假如直接在屏幕DC上操作,那么每画一次,就得更新一次界面,所以会闪屏。如果在缓冲上操作,然后BitBlt给屏幕,就可以尽量避免闪屏。
图形的保存
工程的序列化不用多说,CArchive去弄。保存成bmp位图需要了解下bmp的格式,然后用DIB相关的API将DC的图像数据拎出来,存到文件里。
历史记录的实现
这一部分是我认为比较有趣的部分,也是实现较难的部分,大家日常用word它就有撤销的功能,像PS有历史记录可供恢复,那么这一功能实现起来还真不是那么简单。
看代码:
class CGraphicLog
{
public:
CGraphicLog( CObArray* arr );
~CGraphicLog(); enum { MAX_SAVE = LOG_MAX_SAVE };
enum GOS
{
GOS_NONE,
GOS_ADD,
GOS_DELETE,
GOS_UPDATE,
}; public:
void Clear();
BOOL CanUndo() const;
BOOL CanDo() const;
void Undo(); // 撤消纪录
void Done(); // 恢复纪录
void Operator( GOS, CGraphic*, int, BOOL bClear = TRUE); // 添加操作纪录
void DoneOper( GOS, CGraphic*, int ); // 添加恢复纪录 BOOL Add( CGraphic* pOb ); // 添加数据
BOOL Add( CGraphic* pOb, int ID ); // 添加数据 protected:
void ClearDone();
void ClearUndo();
void ClearArray();
void Delete( CGraphic* pOb );
BOOL AddRef( CGraphic* pOb ); public:
typedef struct GraphicOperation
{
GOS oper;
CGraphic* pGraphic;
int index; CString Trace();
} _GO ; CList<_GO, _GO&> m_listDone;
CList<_GO, _GO&> m_listUndo;
int m_dones;
int m_undos;
CObArray* m_parr;
CMap<CGraphic*, CGraphic*&, int, int&> m_refs; // 引用表
};
几大问题:
- 撤销能不能真正删除数据?不能,否则如何恢复
- 一会恢复一会撤销,对象就是动态创建的,如何管理?引用计数加链表
- 撤销和恢复互为逆操作吗?是
- 只是将对象放进链表里吗?不是,因为对象一旦被修改,就要记录修改前的副本
因此,操作有三种:添加、删除、更改,但组合起来不那么简单。
最核心函数:void Operator( GOS, CGraphic*, int, BOOL bClear = TRUE); // 添加操作纪录
添加操作记录
共有两组链表:撤销记录和恢复记录,记录着操作的类型/对象指针/对象ID。数据在CObArray*m_parr中。增加引用AddRef,去引用Delete,添加Add。
void CGraphicLog::Operator( GOS oper, CGraphic* p, int index, BOOL bClear /*= TRUE*/ )
{
ASSERT_VALID(p); // 每次操作之后,记忆的恢复操作应该全部清除
// 使用者操作时,参数bClear为真
// 撤消操作时,bClear为假
if (bClear) ClearDone(); if (bClear)
{
// * * * 这里会修改引用计数和操作对象数组 * * * // 凡是将对象从m_obArray(*m_parr)移出至(listUndo),那么不增加引用
switch (oper)
{
case GOS_ADD:
// 使用本类的Add(CGraphic*)添加对象并初始化引用计数
ASSERT(!Add(p));
// 因为撤消列表里要保存添加操作,所以引用计数加一
AddRef(p);
// 这样引用计数为二
break;
case GOS_DELETE:
// 将其从原数组中移除(不是删除)
m_parr->RemoveAt(index);
break;
case GOS_UPDATE:
// 更改操作,这时要保存原对象(更改前的)
// 但是修改后的对象是最新创建的,没有引用计数
// 所以还得初始化引用计数
// 此时p为新建备份
ASSERT(!AddRef(p));
break;
default: ASSERT(!"Operation fault!");
}
} if (m_undos == MAX_SAVE)
{
// 如果撤消列表已经满,自动删除列尾
ASSERT(!m_listUndo.IsEmpty());
Delete(m_listUndo.GetTail().pGraphic);
m_listUndo.RemoveTail();
}
else
{
m_undos++;
}
_GO go;
go.index = index;
go.oper = oper;
go.pGraphic = p;
TRACE("LOG OPER %d %s / UN: %d DN: %d REF: %d\n", bClear, go.Trace(), m_undos, m_dones, m_refs[go.pGraphic]); // 添加撤消记录
m_listUndo.AddHead(go);
}
撤销操作
void CGraphicLog::Undo()
{
// * * * 这里会修改引用计数和操作对象数组 * * * if (m_undos == 0)
{
return;
}
TRACE("LOG UNDO ------\n");
m_undos--;
_GO go = m_listUndo.GetHead();
CGraphic* pOb = NULL;
switch (go.oper)
{
case GOS_ADD:
// 撤消添加的,所以为删除操作
// 将其从图像数组中移除,引用计数减一
m_parr->RemoveAt(go.index); // 撤消列表中本操作记录删除(用完了删除),引用计数减一
// 这时要保存恢复操作,要恢复撤消添加
// 所以在listDone里要保存添加操作,引用计数加一
// 总之引用计数减一
Delete(go.pGraphic);
DoneOper(GOS_ADD, go.pGraphic, go.index);
break;
case GOS_DELETE:
// 撤消删除的,所以为添加操作
// 将其移动到图像数组中相应位置,引用计数不变
m_parr->InsertAt(go.index, go.pGraphic); // 撤消之前的对象要保存(移动)到恢复列表中,引用计数不变
// 对象恢复到原始数组,引用计数加一
AddRef(go.pGraphic);
DoneOper(GOS_DELETE, go.pGraphic, go.index);
break;
case GOS_UPDATE:
// 撤消更改,现数组中对象要恢复成撤消之前的
pOb = Convert_To_Graphic(m_parr->GetAt(go.index));
m_parr->ElementAt(go.index) = go.pGraphic; // 所以原对象被保存(移动)进恢复列表,引用计数不变
// 新对象从撤消操作记录列表中移动进对象数组,引用计数不变
// 总之引用计数不变
DoneOper(GOS_UPDATE, pOb, go.index);
break;
default: ASSERT(!"operation fault!");
}
m_listUndo.RemoveHead();
}
恢复操作
void CGraphicLog::Done()
{
// * * * 这里会修改引用计数和操作对象数组 * * * // 恢复操作遵循oper指令 if (m_dones == 0)
{
return;
}
TRACE("LOG DONE ------\n");
m_dones--;
_GO go = m_listDone.GetHead();
CGraphic* pOb = NULL;
switch (go.oper)
{
case GOS_ADD:
// 添加操作
m_parr->InsertAt(go.index, go.pGraphic); // 从保存列表移动至目标数组,引用计数不变
// 添加撤消操作,引用计数加一
// 总之引用计数加一
AddRef(go.pGraphic);
Operator(GOS_ADD, go.pGraphic, go.index, FALSE);
break;
case GOS_DELETE:
// 删除操作
m_parr->RemoveAt(go.index); // 原数组中其被删除,恢复列表删除,引用计数减二
// 唯一保存在撤消列表中,引用计数加一
// 总之引用计数减一
Delete(go.pGraphic);
Operator(GOS_DELETE, go.pGraphic, go.index, FALSE);
break;
case GOS_UPDATE:
// 更改操作
pOb = Convert_To_Graphic(m_parr->GetAt(go.index));
m_parr->ElementAt(go.index) = go.pGraphic; // go.pGraphic 恢复列表->目标数组,引用计数不变
// pOb 目标数组->恢复列表,引用计数不变
Operator(GOS_UPDATE, pOb, go.index, FALSE);
break;
default: ASSERT(!"operation fault!");
}
m_listDone.RemoveHead();
}
引用计数
void CGraphicLog::Delete( CGraphic* pOb )
{
// 删除操作,当且仅当引用计数为1时(无其他引用)删除
ASSERT_VALID(pOb);
int ref;
if (m_refs.Lookup(pOb, ref))
{
ASSERT(ref >= 1);
if (ref == 1)
{
for (int i = 0; i < m_parr->GetSize(); i++)
{
if (m_parr->GetAt(i) == (CObject*)pOb)
{
TRACE("Graphic Delete ID: %d, ADDR: %p In Main Array\n", i, pOb);
m_parr->RemoveAt(i);
break;
}
}
delete pOb;
m_refs.RemoveKey(pOb);
return;
}
m_refs[pOb] = ref - 1;
}
else
{
ASSERT(!"Object not found!");
}
} BOOL CGraphicLog::AddRef( CGraphic* pOb )
{
// 增加引用计数
ASSERT_VALID(pOb);
int ref;
if (m_refs.Lookup(pOb, ref))
{
m_refs[pOb] = ref + 1;
return TRUE;
}
else
{
m_refs[pOb] = 1;
return FALSE;
} // 假如是初始化引用计数,那么返回FALSE
} BOOL CGraphicLog::Add( CGraphic* pOb )
{
// 新建对象后的必须操作
// 向数组中新增对象
// 初始化引用计数
ASSERT_VALID(pOb);
m_parr->Add(pOb);
return AddRef(pOb);
} BOOL CGraphicLog::Add( CGraphic* pOb, int ID )
{
// 只在序列化读取时,将所有图形的引用计数初始化为1
// m_parr之前必须调用SetSize(这样快)
ASSERT_VALID(pOb);
m_parr->ElementAt(ID) = pOb;
return AddRef(pOb);
}
由于代码中有注释(都是为了debug才理清思路写),所以直接上代码了,自己现在也讲不清楚,我想应该还有更好的实现。上述代码都是在引用计数上大作文章,一个计数写错就会导致bug。。
由https://zhuanlan.zhihu.com/p/27350169备份。
MFC图形编辑器的更多相关文章
- canvas图形编辑器
原文地址:http://jeffzhong.space/2017/11/02/drawboard/ 使用canvas进行开发项目,我们离不开各种线段,曲线,图形,但每次都必须用代码一步一步的实现.有没 ...
- MFC-创建MFC图形界面dll
创建MFC图形界面dll 概述: 利用MFC的DLL框架,制作带有图形界面的dll,可以实现很多功能. 流程: 选择静态链接MFC DLL:以免有的库没有. 采用该框架创建的MFC,会自动生产一个MF ...
- MFC图形图像
一.CDC类 CDC类简介 CDC类是一个设备上下文类. CDC类提供了用来处理显示器或打印机等设备上下文的成员函数,还有处理与窗口客户区关联的显示上下文的成员函数.使用CDC的成员函数可以进行所有的 ...
- MFC图形编辑界面工具
一.背景 喔,五天的实训终于结束了,学校安排的这次实训课名称叫高级程序设计实训,但在我看来,主要是学习了Visual C++ .NET所提供的MFC(Microsoft Foundation Clas ...
- 替代PhotoShop:GIMP图形编辑器的使用
GIMP最早是linux环境下用于图形编辑的一款开源软件,目前的功能很已经很丰富,如果使用得当,在很多的图形编辑操作上完全可以替代收费的Photoshop(PS).目前GIMP已经发展成了多平台的开源 ...
- MFC图形绘制——绘制直尺和坐标系
一.实验目的 1.掌握建立MFC应用程序的方法: 2.掌握映射模式. 二.实验内容 1.在MFC中绘制直尺,直尺需要有刻度,类似于日常学生使用的透明塑料直尺,需要建立四个直尺,分别分布在屏幕客户区的上 ...
- [MFC]图形附加alpha透明通道
改动图形而且附加透明通道: 要附加透明度,能够要把图片转化为32位png图片,然后设置对应的alpha值: 1. 怎样把一张图片改动为32位的Png: a) 读取原图片颜色信息 ...
- ada 图形编辑器 - GNAT GPL
The GNAT GPL and SPARK GPL Editions are made available to the free software developers by AdaCore. T ...
- 【Javascript】js图形编辑器库介绍
10个JavaScript库绘制自己的图表 jopen 2015-04-06 18:18:38 • 发布 摘要:10个JavaScript库绘制自己的图表 JointJS JointJS is a J ...
随机推荐
- 算法笔记_194:历届试题 翻硬币(Java)
目录 1 问题描述 2 解决方案 1 问题描述 问题描述 小明正在玩一个“翻硬币”的游戏. 桌上放着排成一排的若干硬币.我们用 * 表示正面,用 o 表示反面(是小写字母,不是零). 比如,可能情 ...
- ci高级使用方法篇之连接多个数据库
在我们的项目中有时可能须要连接不止一个数据库.在ci中怎样实现呢? 我们在本地新建了两个数据库,例如以下截图所看到的: 改动配置文件database.php文件为例如以下格式(读者依据自己数据库的情况 ...
- 纯jascript解决手机端拍照、选图后图片被旋转问题
需要的js1 需要的js2 这里主要用到Orientation属性. Orientation属性说明如下: 旋转角度 参数 0° 1 顺时针90° 6 逆时针90° 8 180° 3 <!DOC ...
- COCOS学习笔记--粒子系统
一.粒子系统的简单介绍 粒子系统是指计算机图形学中模拟特定现象的技术,它在模仿自然现象.物理现象及空间扭曲上具备得天独厚的优势,为我们实现一些真实自然而又带有随机性的特效(如爆炸.烟花.水流)提供了方 ...
- SlackWare安装
Keep It Simple Stupid 01.下载 slackware: http://www.slackware.com/ 中科大: http://mirrors.ustc.edu.cn ...
- Java中初级数值类型的大小, volatile和包装类wrapped type的比较
Java中的初级数值类型 Java是静态类型语言, 所有的变量必须先声明再使用. 其初级类型一共8种: boolean: 数据只包含1bit信息, 但是占空间为8-bit, 默认值为false byt ...
- 速度挑战 - 2小时完成HTML5拼图小游戏
概述 我用lufylegend.js开发了第一个HTML5小游戏——拼图游戏,还写了篇博文来炫耀一下:HTML5小游戏<智力大拼图>发布,挑战你的思维风暴. 详细 代码下载:http:// ...
- 初步了解“C#反射”
来源:http://zhidao.baidu.com/link?url=YzuEaWpYMxYV86bAFVmSAGYtXEzkJ_ndMyZ69QuvNJfikwXvlmtP42hAslGFS2uu ...
- Chrome禁用缓存
Chrome默认对JS和CSS等静态资源进行缓存,对HTML不启用缓存. 在开发阶段,我们想要更改之后马上看到效果,那就必须禁用JS和CSS. 快捷键是F12+F1,F12相当于打开dev-tool, ...
- 【LeetCode】133. Clone Graph (3 solutions)
Clone Graph Clone an undirected graph. Each node in the graph contains a label and a list of its nei ...