Action plan for creating an MFC GUI program

 
運用 Visual Studio 來製作 MFC GUI 程式需要適當的計畫
 
步驟一

第一個步驟當然是想像/設計程式的功能以及使用者的操作界面

這個程式我們運用先前完成的複數 Complex 類別, 設計一個圖形化的界面來輸入兩個 複數, 然後計算 加/減/乘/除 的結果以 圖形及文字 顯示在視窗中

圖形界面如下圖所示:

 

 

 

 

 

 

  1. 畫面上方顯示 實數及虛數部份都在 [-2,2] 範圍內的 複數平面

  2. 畫面上標示座標軸、原點、以及半徑為 1 和 半徑為 2 的圓

  3. 複數平面上有標示三個點, 藍色的為起點, 紅色的為終點, 紫色的為運算的結果

  4. 畫面下方也以文字顯示這三個點的複數值

  5. 使用者可以用滑鼠左鍵在複數平面上點一下, 即設定該點起點, 以滑鼠右鍵在複數平面上點一下, 即設定該點

  6. 游標只要移入複數平面中就由箭頭轉變為十字 , 提示使用者可以用滑鼠左鍵或是右鍵來點選

  7. 任何時候計算的結果除了顯示在畫面下方, 也顯示在複數平面上, 只要起點或是終點有任何改變, 就會重新計算結果

  8. 使用者可以操作的選單包含
    a. 檔案/結束
    b. 運算種類/加, 運算種類/減, 運算種類/乘, 運算種類/除
    c. 說明/關於

    這三個選單項目也可以在右鍵選單中看到 (使用者將滑鼠移到畫面中複數平面之外的地方按下滑鼠右鍵)

  9. 視窗可以縮到最小, 但是不能放到最大, 也不能更改大小

當然一般你在開始設計這種圖形化界面的應用程式時, 只會有一個大概設計的界面圖形, 不會有上面這個完整畫面的...

範例執行程式

下面這些步驟只是摘要, 很多部份需要有相當的說明與練習你才知道遇見問題時該怎樣克服, 雖然說照著做有機會完成, 不過你應該會發現你需要更清楚視窗系統的基本運作機制, 你才有辦法自己獨立設計一個應用程式 , 每一個單一的功能你可以在 Google 上找到很多相關的建議, 可是和你的環境都不見得相同, 所以都不一定能夠順利運作!! 你都需要依照你對於 視窗系統的了解、物件導向機制的了解來判斷和調整...

以下原則上每一個步驟都需要編譯測試, 才會繼續下一步驟的功能, 細節請參考 詳細程式製作過程

步驟二

在 VC2010 中產生 MFC 應用程式專案, 使用 SDI 界面, 編譯測試

步驟三

更改視窗屬性, 使得使用者無法更改視窗大小, 使最大化按鈕失效

BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
    if( !CFrameWndEx::PreCreateWindow(cs) )
        return FALSE;
    // TODO: 在此經由修改 CREATESTRUCT cs 
    // 達到修改視窗類別或樣式的目的
    cs.style &= ~(WS_THICKFRAME | WS_MAXIMIZEBOX);
    //cs.cx = 1000; // SDI 程式無效
    //cs.cy = 500;

    return TRUE;
}

調整視窗大小

SDI 應用程式 + CView 視窗, 比較難處理, MDI 應用程式或是 CFormView 都可以用 ResizeParentToFit()
void CGUIComplexCalcView::OnInitialUpdate()
{
    CView::OnInitialUpdate();

    // TODO: 在此加入特定的程式碼和 (或) 呼叫基底類別
    CClientDC dc(this);
    dc.SetMapMode(MM_LOENGLISH);
    CSize squareInch(1000,1000);
    dc.LPtoDP(&squareInch);
    m_cxInch = squareInch.cx/10.0; // 邏輯上每一英吋的 pixel 數
    m_cyInch = squareInch.cy/10.0;
//    TRACE("%f %f\n", m_cxInch, m_cyInch); // 108.4 108.9

//    short cxInch = dc.GetDeviceCaps(LOGPIXELSX); // 傳回資料不正確
//    short cyInch = dc.GetDeviceCaps(LOGPIXELSY);
//    TRACE("%d %d\n", cxInch, cyInch); // 96,96

    CFrameWnd *pMainWnd = GetParentFrame(); // 視窗寬 4.4 英吋, 高 6 英吋 (含標題列, 選單)
    pMainWnd->SetWindowPos(0,0,0 , (int)(4.4*m_cxInch+0.5), 
                            (int)(6*m_cyInch+0.5), SWP_NOMOVE|SWP_NOZORDER);
    pMainWnd->ShowWindow(SW_SHOW);
}

取消工具列以及狀態列

將 m_wndMenuBar, m_wndToolBar, m_wndStatusBar  相關的都註解掉

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
    if (CFrameWndEx::OnCreate(lpCreateStruct) == -1)
        return -1;

//	BOOL bNameValid;
    // 根據持續值設定視覺化管理員和樣式
    OnApplicationLook(theApp.m_nAppLook);
/*
    if (!m_wndMenuBar.Create(this))
    {
        TRACE0("無法建立功能表列\n");
        return -1;      // 無法建立
    }

    m_wndMenuBar.SetPaneStyle(m_wndMenuBar.GetPaneStyle() | 
        CBRS_SIZE_DYNAMIC | CBRS_TOOLTIPS | CBRS_FLYBY);

	// 防止功能表列在啟動時取得焦點
    CMFCPopupMenu::SetForceMenuFocus(FALSE);

    if (!m_wndToolBar.CreateEx(this,
        TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP | CBRS_GRIPPER | 
        CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC) ||
		!m_wndToolBar.LoadToolBar(theApp.m_bHiColorIcons ? 
            IDR_MAINFRAME_256 : IDR_MAINFRAME))
    {
        TRACE0("無法建立工具列\n");
        return -1;      // 無法建立
    }

    CString strToolBarName;
    bNameValid = strToolBarName.LoadString(IDS_TOOLBAR_STANDARD);
    ASSERT(bNameValid);
    m_wndToolBar.SetWindowText(strToolBarName);

    CString strCustomize;
    bNameValid = strCustomize.LoadString(IDS_TOOLBAR_CUSTOMIZE);
    ASSERT(bNameValid);
    m_wndToolBar.EnableCustomizeButton(TRUE, ID_VIEW_CUSTOMIZE, strCustomize);

    // 允許使用者定義的工具列作業:
    InitUserToolbars(NULL, uiFirstUserToolBarId, uiLastUserToolBarId);

    if (!m_wndStatusBar.Create(this))
    {
        TRACE0("無法建立狀態列\n");
        return -1;      // 無法建立
    }
    m_wndStatusBar.SetIndicators(indicators, sizeof(indicators)/sizeof(UINT));

    // TODO: 如果不希望工具列和功能表列為可停駐,請刪除這 5 行
    m_wndMenuBar.EnableDocking(CBRS_ALIGN_ANY);
    m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY);
    EnableDocking(CBRS_ALIGN_ANY);
    DockPane(&m_wndMenuBar);
    DockPane(&m_wndToolBar);

    // 啟用 Visual Studio 2005 樣式停駐視窗行為
    CDockingManager::SetDockingMode(DT_SMART);
    // 啟用 Visual Studio 2005 樣式停駐視窗自動隱藏行為
    EnableAutoHidePanes(CBRS_ALIGN_ANY);

    // 啟用工具列和停駐視窗功能表取代
    EnablePaneMenu(TRUE, ID_VIEW_CUSTOMIZE, strCustomize, ID_VIEW_TOOLBAR);

    // 啟用快速 (Alt+拖曳) 工具列自訂
    CMFCToolBar::EnableQuickCustomization();

    if (CMFCToolBar::GetUserImages() == NULL)
    {
        // 載入使用者定義的工具列影像
        if (m_UserImages.Load(_T(".\\UserImages.bmp")))
        {
            CMFCToolBar::SetUserImages(&m_UserImages);
        }
    }

    // 啟用功能表個人化 (最近使用的命令)
    // TODO: 定義您自己的基本命令,確定每個下拉式功能表都至少有一個基本命令。
    CList<UINT, UINT> lstBasicCommands;

    lstBasicCommands.AddTail(ID_FILE_NEW);
    lstBasicCommands.AddTail(ID_FILE_OPEN);
    lstBasicCommands.AddTail(ID_FILE_SAVE);
    lstBasicCommands.AddTail(ID_FILE_PRINT);
    lstBasicCommands.AddTail(ID_APP_EXIT);
    lstBasicCommands.AddTail(ID_EDIT_CUT);
    lstBasicCommands.AddTail(ID_EDIT_PASTE);
    lstBasicCommands.AddTail(ID_EDIT_UNDO);
    lstBasicCommands.AddTail(ID_APP_ABOUT);
    lstBasicCommands.AddTail(ID_VIEW_STATUS_BAR);
    lstBasicCommands.AddTail(ID_VIEW_TOOLBAR);
    lstBasicCommands.AddTail(ID_VIEW_APPLOOK_OFF_2003);
    lstBasicCommands.AddTail(ID_VIEW_APPLOOK_VS_2005);
    lstBasicCommands.AddTail(ID_VIEW_APPLOOK_OFF_2007_BLUE);
    lstBasicCommands.AddTail(ID_VIEW_APPLOOK_OFF_2007_SILVER);
    lstBasicCommands.AddTail(ID_VIEW_APPLOOK_OFF_2007_BLACK);
    lstBasicCommands.AddTail(ID_VIEW_APPLOOK_OFF_2007_AQUA);
    lstBasicCommands.AddTail(ID_VIEW_APPLOOK_WINDOWS_7);

    CMFCToolBar::SetBasicCommands(lstBasicCommands);
*/
    return 0;
}

設定應用程式標題

void CMainFrame::OnUpdateFrameTitle(BOOL bAddToTitle)
{
    // TODO: 在此加入特定的程式碼和 (或) 呼叫基底類別
    CFrameWndEx::OnUpdateFrameTitle(bAddToTitle);
    // order is important
    SetWindowText(_T("GUI Complex Calculator")); // overrides document title
}
步驟四

設定 mapping mode 為 MM_LOENGLISH (座標軸一單位是 1/100 英吋)

void CGUIComplexCalcView::OnPrepareDC(CDC* pDC, CPrintInfo* pInfo)
{
    // TODO: 在此加入特定的程式碼和 (或) 呼叫基底類別
    GetClientRect(&m_rectClient); //裝置座標
    pDC->SetViewportOrg(CPoint(m_rectClient.right/2, 
        m_rectClient.bottom/2-(int)(0.55*m_cyInch))); //裝置座標
    pDC->SetMapMode(MM_LOENGLISH);

    CView::OnPrepareDC(pDC, pInfo);
}

計算複數平面區域的範圍

void CGUIComplexCalcView::OnInitialUpdate()
{
    ...

    int cx_border = GetSystemMetrics(SM_CXFRAME);
    int cy_border = GetSystemMetrics(SM_CYFRAME);
    int cy_caption = GetSystemMetrics(SM_CYCAPTION);
    CRect window_rect;
    pMainWnd->GetWindowRect(&window_rect);
    CPoint client_top_left(0, 0);
    pMainWnd->ClientToScreen(&client_top_left);
    int menu_height = client_top_left.y - window_rect.top - cy_caption - cy_border;
//    TRACE("cy_caption=%d menu_height=%d client_top_left.y=%d cy_border=%d\n",
//           cy_caption, menu_height, client_top_left.y, cy_border);

    CRect rectClient; GetClientRect(&rectClient); //裝置座標
    int xmargin = (int)((4.4-4.12)*m_cxInch - 2*cx_border)/2;
    int ymargin = (int)(((6-4.12)*m_cyInch - 2*cy_border - 
                        cy_caption - menu_height)/2.0 - 0.55*m_cyInch) ;
//    TRACE("xmargin=%d ymargin=%d right=%d bottom=%d\n", 
//          xmargin, ymargin, rectClient.right, rectClient.bottom);
    m_rectHit = CRect(xmargin, ymargin, 
        xmargin+(int)(4.1*m_cxInch), ymargin+(int)(4.1*m_cyInch));
}
4.12 = 4 + 2*0.04 + 2*0.02
步驟五

繪製座標軸

void CGUIComplexCalcView::OnDraw(CDC* pDC)
{
    int i;
    CGUIComplexCalcDoc* pDoc = GetDocument();
    ASSERT_VALID(pDoc);
    if (!pDoc)
        return;

    // TODO: 在此加入原生資料的描繪程式碼
	...
    // x-axis
    CPen bAxisPen(PS_SOLID, 2, RGB(255, 0, 0));
    pDC->SelectObject(&bAxisPen);
    pDC->MoveTo(-200, 0);
    pDC->LineTo(200, 0);
    pDC->MoveTo(190,5); pDC->LineTo(200, 0);pDC->LineTo(190,-5); 
    for (i=-200; i<=200; i+=100)
    {
        pDC->MoveTo(i, -6); 
        pDC->LineTo(i, 6);
    }

    // y-axis
    pDC->MoveTo(0, -200);
    pDC->LineTo(0, 200);
    pDC->MoveTo(-5,190); pDC->LineTo(0,200);pDC->LineTo(5,190); 
    for (i=-200; i<=200; i+=100)
    {
        pDC->MoveTo(-6, i); 
        pDC->LineTo(6, i);
    }
	...

標示參考點

    // labels
    pDC->TextOut(5,-3, CString("(0+0i)"));
    pDC->TextOut(168,-5, CString("(2+0i)"));
    pDC->TextOut(5,198, CString("(0+2i)"));

繪製複數平面區域

    // bounding rectangle
    CPen bPen(PS_SOLID, 3, RGB(0, 0, 255));
    pOldPen = (CPen*) pDC->SelectObject(&bPen);
    pDC->Rectangle(CRect(-204, 204, 204,-204));

繪製參考單位圓及2單位圓

    // reference circle
    CBrush *pOldBr = (CBrush*) pDC->SelectStockObject(NULL_BRUSH);
    CPen bPen2(PS_DASH, 1, RGB(200, 200, 200));
    CPen *pOldPen = (CPen*) pDC->SelectObject(&bPen2);
    pDC->Ellipse(-100, 100, 100, -100);
    pDC->Ellipse(-200, 200, 200, -200);
步驟六

加入 Complex.h 以及 Complex.cpp

在 Complex.cpp 的第一列加入 #include "stdafx.h"

加入一個界面 convertPoint() 將 Complex 型態的複數點轉換為 CPoint 型態的座標點

CPoint Complex::convertPoint()
{
    return CPoint((int)(m_real*100), (int)(m_imaginary*100));
}

繪製 3 個點以及畫面下方的說明文字

void CGUIComplexCalcView::OnDraw(CDC* pDC)
{
    ...
    // end point
    pDC->MoveTo(m_endPt.convertPoint()+CPoint(-5,5));
    pDC->LineTo(m_endPt.convertPoint()+CPoint(5,-5));
    pDC->MoveTo(m_endPt.convertPoint()+CPoint(5,5));
    pDC->LineTo(m_endPt.convertPoint()+CPoint(-5,-5));
    ...
    // start point
    pDC->MoveTo(m_startPt.convertPoint()+CPoint(-5,5));
    pDC->LineTo(m_startPt.convertPoint()+CPoint(5,-5));
    pDC->MoveTo(m_startPt.convertPoint()+CPoint(5,5));
    pDC->LineTo(m_startPt.convertPoint()+CPoint(-5,-5));

    // text descriptions
    pDC->TextOutW(-200, -210, 
        CString("請在上面複數平面中以滑鼠左鍵及右鍵選擇兩點"));

    ostrstream sstr; m_startPt.print(sstr); sstr<<ends;
    CString line("起點: "); line += sstr.str(); sstr.freeze(false);
    pDC->SetTextColor(RGB(0,0,255));
    pDC->TextOutW(-190, -240, line);
    
    ostrstream sstr2; m_endPt.print(sstr2); sstr2<<ends; 
    CString line2("終點: "); line2 += sstr2.str(); sstr2.freeze(false);
    pDC->SetTextColor(RGB(255,0,0));
    pDC->TextOutW(-190, -260, line2);

    CString line3;
    m_resultPt = m_startPt;
    m_resultPt.add(m_endPt);
    line3 = "起點 + 終點 = ";
    CPoint pt = m_resultPt.convertPoint();
    pDC->LPtoDP(&pt);

    ostrstream sstr3; m_resultPt.print(sstr3); sstr3<<ends; 
    line3 += sstr3.str(); sstr3.freeze(false);
    pDC->SetTextColor(RGB(192,0,128));
    if (!m_rectHit.PtInRect(pt))
        line3 += "  (outside the box)";
    pDC->TextOutW(-190, -290, line3);

    // result point
    pt = m_resultPt.convertPoint();
    CPen bResultPen(PS_SOLID, 3, RGB(192, 0, 128));
    pDC->SelectObject(&bResultPen);
    pDC->MoveTo(pt+CPoint(-6,0));
    pDC->LineTo(pt+CPoint(6,0));
    pDC->MoveTo(pt+CPoint(0,6));
    pDC->LineTo(pt+CPoint(0,-6));
    CRect resultRect(pt.x-6, pt.y-6, pt.x+7, pt.y+7);
    pDC->Ellipse(&resultRect);

    pDC->SelectObject(pOldPen);
    pDC->SelectObject(pOldBr);
	...
}
步驟七

測試游標是否位於複數平面區間, 如果游標在區間內則更改游標形狀

處理 WM_SETCURSOR 訊息

BOOL CGUIComplexCalcView::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
{
    // TODO: 在此加入您的訊息處理常式程式碼和 (或) 呼叫預設值
    CPoint point;
    GetCursorPos(&point);
    pWnd->ScreenToClient(&point);
//    TRACE("point=(%d,%d)\n", point.x, point.y);
    if (m_rectHit.PtInRect(point))
    {
        ::SetCursor(AfxGetApp()->LoadStandardCursor(IDC_CROSS));
        return TRUE;
    }
    return CView::OnSetCursor(pWnd, nHitTest, message);
}
步驟八

處理滑鼠左鍵以及右鍵的輸入

處理 WM_LBUTTONUPWM_RBUTTONUP 兩個訊息

void CGUIComplexCalcView::OnLButtonUp(UINT nFlags, CPoint point)
{
    // TODO: 在此加入您的訊息處理常式程式碼和 (或) 呼叫預設值
//    TRACE("point.x=%d point.y=%d\n", point.x, point.y);
    if (m_rectHit.PtInRect(point))
    {
        m_startPt = pointToComplex(point);
        Invalidate();
    }
    CView::OnLButtonUp(nFlags, point);
}


void CGUIComplexCalcView::OnRButtonUp(UINT nFlags, CPoint point)
{
    // TODO: 在此加入您的訊息處理常式程式碼和 (或) 呼叫預設值
    if (m_rectHit.PtInRect(point))
    {
        m_endPt = pointToComplex(point);
        Invalidate();
        CView::OnRButtonUp(nFlags, point);
    }
    else
    {
        ClientToScreen(&point);
        OnContextMenu(this, point);
    }
}

// 將平面上一個 CPoint 點轉換為 Complex 複數
Complex CGUIComplexCalcView::pointToComplex(CPoint point)
{
    CPoint origin((int)((m_rectHit.left+m_rectHit.right+0.5)/2.0), 
                  (int)((m_rectHit.top+m_rectHit.bottom+0.5)/2.0));
    return Complex((point.x - origin.x) / m_cxInch,
                   -(point.y - origin.y) / m_cyInch);
}
步驟九

調整設計選單

處理選單訊息 IDM_ADD, IDM_SUBTRACT, IDM_MULTIPLY, IDM_DIVIDE

void CGUIComplexCalcView::OnAdd()
{
    // TODO: 在此加入您的命令處理常式程式碼
    m_typeOfOperation = 0;
    Invalidate();
}


void CGUIComplexCalcView::OnSubtract()
{
    // TODO: 在此加入您的命令處理常式程式碼
    m_typeOfOperation = 1;
    Invalidate();
}


void CGUIComplexCalcView::OnMultiply()
{
    // TODO: 在此加入您的命令處理常式程式碼
    m_typeOfOperation = 2;
    Invalidate();
}


void CGUIComplexCalcView::OnDivide()
{
    // TODO: 在此加入您的命令處理常式程式碼
    m_typeOfOperation = 3;
    Invalidate();
}

void CGUIComplexCalcView::OnDraw(CDC* pDC)
{
    ...
    CString line3;
    m_resultPt = m_startPt;
    switch (m_typeOfOperation)
    {
    case 0:
        line3 = "起點 + 終點 = ";
        m_resultPt.add(m_endPt);
        break;
    case 1:
        line3 = "起點 - 終點 = ";
        m_resultPt.subtract(m_endPt);
        break;
    case 2:
        line3 = "起點 * 終點 = ";
        m_resultPt.multiply(m_endPt);
        break;
    case 3:
        line3 = "起點 / 終點 = ";
        m_resultPt.divide(m_endPt);
    }
    CPoint pt = m_resultPt.convertPoint();
    pDC->LPtoDP(&pt);
    ...
}


void CGUIComplexCalcView::OnUpdateAdd(CCmdUI *pCmdUI)
{
    // TODO: 在此加入您的命令更新 UI 處理常式程式碼
    pCmdUI->SetCheck(m_typeOfOperation == 0);
}


void CGUIComplexCalcView::OnUpdateSubtract(CCmdUI *pCmdUI)
{
    // TODO: 在此加入您的命令更新 UI 處理常式程式碼
    pCmdUI->SetCheck(m_typeOfOperation == 1);
}


void CGUIComplexCalcView::OnUpdateMultiply(CCmdUI *pCmdUI)
{
    // TODO: 在此加入您的命令更新 UI 處理常式程式碼
    pCmdUI->SetCheck(m_typeOfOperation == 2);
}


void CGUIComplexCalcView::OnUpdateDivide(CCmdUI *pCmdUI)
{
    // TODO: 在此加入您的命令更新 UI 處理常式程式碼
    pCmdUI->SetCheck(m_typeOfOperation == 3);
}
步驟十

設計滑鼠右鍵選單

已經自動顯示右鍵選單, 但是還需要設定只有當滑鼠移到視窗下方指定區間才能動作

void CGUIComplexCalcView::OnContextMenu(CWnd* /* pWnd */, CPoint point)
{
//    TRACE("point.x=%d point.y=%d\n", point.x, point.y);
#ifndef SHARED_HANDLERS
    CPoint sPoint(point);
    ScreenToClient(&sPoint);
    if (!m_rectHit.PtInRect(sPoint))
        theApp.GetContextMenuManager()->
            ShowPopupMenu(IDR_POPUP_EDIT, point.x, point.y, this, TRUE);
#endif
}

處理選單訊息 ID_APP_EXIT 及 ID_APP_ABOUT

void CGUIComplexCalcView::OnAppExit()
{
    // TODO: 在此加入您的命令處理常式程式碼
    AfxGetMainWnd()->PostMessageW(WM_CLOSE);
}


void CGUIComplexCalcView::OnAppAbout()
{
    // TODO: 在此加入您的命令處理常式程式碼
    ((CGUIComplexCalcApp*)AfxGetApp())->OnAppAbout();
}
步驟十一

更改應用程式的圖示 (icon)

完整程式製作過程 (android com.adobe.flashplayer.apk)
範例程式碼

C++ 物件導向程式設計課程 首頁

製作日期: 04/03/2014 by 丁培毅 (Pei-yih Ting)
E-mail: pyting@mail.ntou.edu.tw TEL: 02 24622192x6615
海洋大學 電機資訊學院 資訊工程學系 Lagoon