天天看點

對“一道刁鑽面試題”的VC解答

    要求:

    1.任何語言 任何形式(web,winform,flash,flex,silverlight)等等。。

    2.實作内容

      a.初始化一個面闆,面闆内随機分布着一些按鈕  按鈕上有一些随機的數字。

      b.有一個按鈕 名字叫“新增節點” 點選 該按鈕後 可以向面闆内随機添加新的 按鈕。

      c.任意順序點選面闆内的按鈕。按順序将所點按鈕用線條連接配接。并且将按鈕的 數值進行累加 顯示到 文本框。

      d.回放 功能。 有一個名叫 “回放的按鈕” 點選該按鈕後 将所有操作慢動作回放。包括增加節點 和 連接配接 的一切操作。完整再現。

    我在前幾天看到這個題目,自覺是不難,也有一些人說到用到連結清單,這是不錯的。而且這些實作起來并不是那麼難,隻是需要一定的耐心。而吸引我要把它用VC實作出來的,并不是這幾個功能本身,而是如何在兩個按鈕之間繪制一個箭頭型的連線,吸引了我的興趣。為什麼這麼說呢,因為按鈕随機出現,是以最簡單的方法,我在兩個按鈕的中心點繪制一條線段就可以了,這樣當然是最簡單的。不過我還想讓顯示效果更理想化一點,也就是在被指向按鈕的線段端點繪制一個三角形的箭頭,并且:這個箭頭不能覆寫到按鈕上,也就是我希望箭頭連接配接到按鈕的矩形邊框上面,做好的程式如下圖所示:

    

對“一道刁鑽面試題”的VC解答

    在上圖中,可以看到實際上每個箭頭都是連接配接按鈕的中心點的,而且箭頭被準确的繪制在合适的位置(上圖的按鈕有些小,是以效果不夠明顯)。是以,這裡的關鍵是要計算出連接配接線和按鈕邊框的交點。我是這樣來做的:首先,在建立按鈕時,我以按鈕的中心點為圓點,計算出中心點到四個頂點的四條射線的角度,由于我是使用 atan 函數來計算兩個按鈕連線的角度,是以射線的角度也使他落在(-90~270)範圍。如下圖示意:

對“一道刁鑽面試題”的VC解答

    上圖的圓周範圍是從-90到270度,采用的坐标都是計算機的螢幕坐标為準(Y正方向向下,和數學中的笛卡爾坐标的Y軸方向相反),角度的正方向也是順時針(也是和數學中的方向反向)。為了簡單直覺,上面的角度都使用角度表示,在實際代碼中都是采用弧度。大緻方法是:

    (1)首先計算出兩個按鈕中心連線的夾角(-90~270);

    (2)根據夾角和按鈕資訊中的 四個對角線射線角 (angle[4])的大小關系,判斷出連線和按鈕相交與哪一個邊緣。

    (3)由于按鈕邊緣上的x,y至少有一個是可知的,是以根據 alpha 角度計算出交點坐标的位置。

    大概過程如上,是以代碼也就會很直覺了:

對“一道刁鑽面試題”的VC解答
對“一道刁鑽面試題”的VC解答

CODE_BUTTON_AND_LINE

//描述一個按鈕

class CNumButton

{

public:

    int left,top,right,bottom;

    //按鈕的對角線的4個角度

    double angle[4];

...

    //指定按鈕的中心點和高度,寬度,數字

    void Init(int cx, int cy, int width, int height, int number)

    {

        double a;

        left = cx - width/2;

        top = cy - height/2;

        right = cx + width/2;

        bottom = cy + height/2;

        num = number;

        //計算對角線角度

        a = atan(((double)height)/width);

        angle[0] = -a;

        angle[1] = a;

        angle[2] = M_PI - a;

        angle[3] = M_PI + a;

    }

};

//描述兩個按鈕的連接配接線

class CLine

private:

    int x0, y0; //From,出發點

    int x1, y1; //To,指向點

    //三角箭頭的點數組

    POINT ptsArrow[3];

    //計算角度 from POINT0 -> to POINT1

    double GetAngle(int x0, int y0, int x1, int y1)

        double angle;

        if(x1==x0)

        {

            if(y1>=y0) return M_PI_2; //90 度

            else return (3 * M_PI_2); //270

        }

        angle = atan(((double)(y1-y0))/(x1-x0));

        if(x1 < x0) angle += M_PI; //如果to在from左側,則需要增加180度

        return angle;

    //擷取連線與按鈕邊緣相交的點坐标

    void GetSidePoint(CNumButton* btn, double angle, int *pX, int *pY)

        if( angle < btn->angle[0] || angle > btn->angle[3]) //上邊緣

            *pY = btn->top;

            *pX = btn->GetCX() - (int)(btn->GetHeigth()/2 * tan(M_PI_2-angle) + 0.5);

        else if(angle < btn->angle[1]) //右邊緣

            *pX = btn->right;

            *pY = btn->GetCY() + (int)(btn->GetWidth()/2 * tan(angle) + 0.5);

        else if(angle < btn->angle[2]) //下邊緣

            *pY = btn->bottom;

            *pX = btn->GetCX() + (int)(btn->GetHeigth()/2 * tan(M_PI_2-angle) + 0.5);

        else if(angle < btn->angle[3]) //左邊緣

            *pX = btn->left;

            *pY = btn->GetCY() - (int)(btn->GetWidth()/2 * tan(angle) + 0.5);

    void Init(CNumButton* fromBtn, CNumButton* toBtn)

        int arrowsize = 20; //箭頭大小

        double angle0, angle1;

        int cx0 = fromBtn->GetCX();

        int cy0 = fromBtn->GetCY();

        int cx1 = toBtn->GetCX();

        int cy1 = toBtn->GetCY();

        angle0 = GetAngle(cx0, cy0, cx1, cy1);

        angle1 = GetAngle(cx1, cy1, cx0, cy0);

        //擷取線段兩個端點

        GetSidePoint(fromBtn, angle0, &x0, &y0);

        GetSidePoint(toBtn, angle1, &x1, &y1);

        //設定箭頭

        ptsArrow[0].x = x1;

        ptsArrow[0].y = y1;

        //擷取三角形箭頭的其他兩個端點

        ptsArrow[1].x = x1 + (int)(arrowsize * cos(angle1-M_PI/12) + 0.5);

        ptsArrow[1].y = y1 + (int)(arrowsize * sin(angle1-M_PI/12) + 0.5);

        ptsArrow[2].x = x1 + (int)(arrowsize * cos(angle1+M_PI/12) + 0.5);

        ptsArrow[2].y = y1 + (int)(arrowsize * sin(angle1+M_PI/12) + 0.5);

  

    此外,在題目要求中提到的一些功能,我定義了一個類:CSolution,用它輔助Windows程式完成示範,繪制等大部分功能。在Solution中儲存了一個按鈕連結清單,一個動作記錄連結清單。其中按鈕連結清單的每個節點都一定包含一個按鈕,每個按鈕節點還包含一個連接配接線的指針(CLine* line,初始為NULL),隻有當這個按鈕被點選且能和之前的按鈕建立連接配接關系,我們就為這個按鈕的CLine*配置設定指向相應的對象。換句話說,除了第一次點選時,無法建立CLine*的指向,之後的每次點選都能為按鈕配置設定CLine*的指向。

    在前面講了太多和這個題考察内容無關的方面,下面還是再說說這道題考察的關鍵部分吧(盡管問題很直覺)。

    首先我們需要定義了兩個連結清單來儲存資訊:

    Buttons連結清單:實際上儲存了視窗上所有的按鈕和連接配接線對象。每個節點都包含一個指向 Button 和一個指向 Line 的指針。注意,每個節點的Button一定存在,而Line可能為NULL。

    Actions連結清單:儲存了目前動作的可重制資訊:包括,目前動作類型(增加按鈕/點選按鈕),目前的Sum值(按鈕數字總和)等等。

    連結清單的節點定義如下: 

對“一道刁鑽面試題”的VC解答
對“一道刁鑽面試題”的VC解答

CODE_SOLUTION

//動作類型

#define    ACTION_ADDBUTTON    0 //增加按鈕

#define ACTION_CLICKBUTTON    1 //按按鈕

//儲存繪制對象的節點

typedef struct _NODE_BUTTON

    CLine        *line;        //相關聯的線

    CNumButton    *btn;            //按鈕

    struct _NODE_BUTTON *prev;    //雙向連結清單

    struct _NODE_BUTTON *next;

} NUMBUTTON, *LPNUMBUTTON;

//記錄動作的節點

typedef struct _NODE_ACTION

    int            type;        //動作類型

    int            sum;        //回放時應該顯示的總和

    CNumButton *btn;    //相關聯的按鈕

    struct _NODE_ACTION *prev;

    struct _NODE_ACTION *next;

} NUMACTION;

//解決問題

class CSolution

    CNumButton *lastBtn;    //最後點選的按鈕(連線的尾部節點)

    NumList<NUMBUTTON> buttons;        //儲存所有按鈕的連結清單

    NumList<NUMACTION> actions;        //儲存所有動作的連結清單

    int nSum;            //數字的總和

    bool bDrawRegion; //是否繪制region(按鈕的可出現位置)

    bool bPlaying;        //是否正在回放?

    此外剩餘的主要技巧基本都是使用 Platform SDK 的傳統Windows 開發技術。特别的,這個例子最初的目的如其名稱,它也展示和練習了 Rebar ,ToolBar, StatuBar 等 CommonCtrl 的使用。在工具欄上使用了 CHEVRON (“>>”)按鈕。該按鈕的顯示控制是由系統的Rebar視窗已經實作的。當Rebar的Band小于它的理想寬度時,ReBar就會在這個Band的邊緣顯示“>>"按鈕。點選“>>”按鈕時應用程式需要彈出一個上下文菜單來顯示那些顯示不完全的項目。在這裡為了給菜單項目顯示左側的圖示,我又對上下文菜單使用了自定義繪制(菜單和工具欄使用的是同一個ImageList)。效果如下圖所示:

對“一道刁鑽面試題”的VC解答

    有一點不是很好的是,當我對客戶區整個重新整理時,我發現 Rebar 的顯示同時也失效了(Rebar上的Bands會顯示不正常),為了不強制rebar重繪,我隻好把客戶區的畫布的頂端向下增加足夠的高度。

    最後工作列上有一個按鈕,是“顯示Region”,它是CSolution内維護的一個HRGN(區域),主要目的,是我在添加一個按鈕後,在畫布中把這個按鈕占據的區域删去,這樣盡可能使增加按鈕時,他們不至于很快重疊在一起。

    當(元素)按鈕重疊時,這裡又有一個小的技巧。即,我們按照 Z 次序 繪制他們,但是在滑鼠點選去嘗試捕獲對象時,則要沿着和繪制相反的順序捕獲。例如,我在這裡繪制按鈕,是從連結清單頭部 繪制到 連結清單的尾部,(按照添加順序),但是在嘗試捕獲時,則需要從連結清單尾部檢索到頭部,這是因為位于最上面的元素是最後繪制的,但是也是最可能被最首先點選到的。

    最後是源代碼的下載下傳連結(修複了NumList模闆類定義中的一個BUG,該BUG導緻擷取List的元素個數不正确):

    BUG修複:

    (1)修複了NumList模闆類定義中的一個BUG,該BUG導緻擷取List的元素個數不正确。

    (2)修複在回放過程中,有些動作中,記錄Sum值 的TextBox沒有及時重新整理的 BUG。

    【附加另一道題目】:為了防止影響對方公司的面試,這裡就隐去該題目的出處。這道題目非常基礎和簡單,這裡作為一種練習。題目要求是:

    使用Win32 ( 不用MFC ),寫一個Windows程式,實作功能:

    (1) 視窗啟動時最大化 ;(2) 視窗的背景色為指定顔色;(3) 左上角顯示滑鼠客戶區坐标;(4) 顯示一張牌, 按方向鍵随之一動,每次移動1像素,并保證不移出視窗。(5) 點選換牌。

    程式運作效果截圖是:

對“一道刁鑽面試題”的VC解答

    這個程式的關鍵是在于如何防止卡片移動時的閃爍,實際上這裡我采用了一種不能應用于普通情況,而僅适用與該題題意的比較投機的方法去防閃爍(因為這個題目中視窗背景是純色的,在實際應用中未必有這麼好的條件)。“閃爍”是windows程式在繪制方面(GDI)的一個經典話題了。當你使用GDI,(而非DirectX,OPENGL之類),那麼不可能不接觸到它。當然,如何防止閃爍主要取決于開發人員對視窗繪制的相關消息和過程的了解和掌握,也就是首先要弄清楚閃爍是如何引起的,然後有針對性的盡最大可能減少閃爍的發生。而不應該僅隻知道一些 double buffer 之類的術語(卻不知是以)。