vs2015居然能夠完美打開工程,哈哈能夠直接生成類圖了。因爲內容較多,因此根據內容的重要性會安排詳略。git
https://github.com/bajdcc/GraphEditor/releases/tag/1.0github
主要的內容:算法
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。。