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\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備份。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM