zoukankan      html  css  js  c++  java
  • MFC图形编辑器

    v2-eb2ae043caf58722b88ad084bccda1bd_1200x500

    前言

    vs2015竟然可以完美打开工程,哈哈可以直接生成类图了。由于内容较多,所以根据内容的重要性会安排详略。

    https://github.com/bajdcc/GraphEditor/releases/tag/1.0

    主要的内容:

    1. MFC的基本使用介绍
    2. 4种图形的绘制
    3. 图形的事件处理
    4. 撤销与恢复功能的实现
    5. 其他功能

    介绍

    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
    ", 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 ------
    ");
        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 ------
    ");
        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
    ", 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备份。

  • 相关阅读:
    problems_springmvc
    skills_eclipse
    problems_azkaban
    CentOS7与CentOS6的不同
    2021暑期cf加训2
    2021牛客暑期多校训练营4
    2021牛客暑期多校训练营3
    2021暑期cf加训1
    2021牛客暑期多校训练营2
    10.git rm 移除文件
  • 原文地址:https://www.cnblogs.com/bajdcc/p/8972988.html
Copyright © 2011-2022 走看看