使用C++和Directx開發GUI(三)

使用C++和Directx開發GUI(三)
 
 

歡迎回到"使用C++和DX開發GUI"的第三部分.(這裏是第一部分和第二部分).接着我們的主題(描述我如何爲我未來的遊戲構建GUI),本文將探討建造GUI所需的一些通用控件.我們將詳細描述幾種不同的控件形式,包括按鈕,列表框,文本框等等.

這一節並不像其他章節那樣有很多的代碼--這主要是因爲我們程序員對於GUI的外觀是很挑剔的.我們喜歡把我們的按鈕,文本框和GUI做的看起來獨一無二,並且符合我們自己的審美標準.這樣的結果是,每個人的控件代碼都很不同,而且不會想要我的特殊的繪製代碼.此外,寫繪製GUI元素的代碼是很有趣的,事實上,以我來看,這是在寫GUI過程中最有趣的部分了.現在繼續.

一個很重要的問題是,在我們開始之前-把你的gui_window析構函數做成虛函數.在第二部分裏我沒有提到這一點,因爲我們沒有從gui_window中派生出任何子類,但是現在我提出這一點-把你的gui_window和所有它的派生類的析構函數做成虛函數是很明智的做法,因爲這將確保沒有內存泄漏--由於派生的析構函數沒有被調用.小心C++的陷阱.

說完這點之後,讓我們首先判斷我們需要什麼樣的GUI控件.

我們需要的GUI控件

我不想花太多的時間來爲我的GUI開發控件;我只會專注於最簡單的控件集.所以,我先列出我認爲是最小控件集的控件:

1.靜態文本,圖標和組合框(最重要).這些空間將對話框中的其他控件標誌或分組.靜態控件很重要;我們可能不需要幀控件,但它非常簡單,並且在某些情況下能夠使對話框易於導航,所以我會包括它.圖標控件也很簡單,但是應該能夠表現動畫-爲我們的對話框和菜單提供很酷的背景動畫(神偷:黑暗計劃).

2.按鈕和選擇框(最重要).特殊形式的按鈕不是必需的,然而大多數的遊戲不能沒有基本的按鈕和選擇框.

3.列表框(重要).我發現列表框,特別是多列列表控件,在創建遊戲GUI時是不可或缺的.他們的應用無所不在.你需要一個智能的,重量級的列表控件,和windows的列表控件一樣好或者更爲出色.對我而言,列表控件是最難開發的控件了.

4.滑動條和滾動條(重要).最常見於音量控制.壞消息是我們可能需要水平和垂直滾動條,好消息是他們很相似所以開發很簡單.

5.文本框(最重要).你必須能夠鍵入你的mega-3133t,super-kewl玩家標誌,對吧?

6.進度條-對顯示生命值是必需的,"我快要裝載好了!"等等情況也是如此.

這裏缺少的是紡錘狀按鈕(spin button),單選框(我們可以用一個單選列表框代替),下拉組合框(同樣我們可以用列表框代替)以及樹狀控件.通過設計巧妙的列表控件來縮進特定物體,我們能夠實現樹狀列表德功能.

由於我的遊戲並沒有足夠的GUI來保證表狀控件,所以在此沒有包含它,雖然你可能會需要.

即使有這些遺漏,上述"最小"列表可能看上去還是很繁雜,但是我們能夠簡化一點兒.

把它打破:組合簡單空間來實現複雜控件

如果我們意識到複雜控件僅僅是簡單控件的巧妙組合,列表就會更易於控制.例如,一個滾動條基本上只是兩個按鈕和一個滑動條.一個選擇框是一個靜態文本和兩個按鈕(一個"打開"按鈕,一個"關閉"按鈕).一個平面按鈕能夠使用三個圖標控件來實現(僅僅顯示/隱藏適當的圖標來使按鈕顯得被按下),這樣你能夠重用你的繪製代碼.如果你的確沒有時間,你甚至可以把一個進度條當作滑動條來用,雖然我更傾向於是用一個獨立的控件.

然而,這樣做是有缺陷的,名義上你的GUI控件會比他們實際需要的佔用更多的系統資源.仔細考慮它-每個控件是一個窗體.讓我們說你使用了重用法則創建了一個實際上是三個靜態控件的按鈕控件.那麼每個按鈕就是三個窗體.現在你使用兩個按鈕控件創建一個滾動條,那就是每個滾動條6個窗體.使用水平和垂直滾動條創建一個列表控件,那麼每個列表就是12個窗體.它增加得很快.

所以這就是另一個經典的關於"我能多快的開發"和"我會使用多少資源"的矛盾的例子.如果你需要一個高性能,沒有浪費的GUI,從基礎來開發每一個控件.如果你想要快速開發,那就不要介意性能損失,你或許會選擇開發控件以使實際上繪製到屏幕上的是靜態控件,所有其他控件都是由靜態控件組合而成的.

我開發GUI的時候,我盡力在兩個極端之間取得良好的平衡.

現在,讓我們開始關注每個控件的實際開發,從每個人最喜歡的靜態標誌開始吧.

我們需要關注三種靜態控件:靜態文本控件,靜態圖標控件和框架控件.這三種控件都很簡單,因爲他們不接收消息-他們所作的只是在某個位置繪製本身而已.

靜態文本控件是你將開發的最簡單的控件了-僅僅在窗口的左上角繪製窗口的標題,就行了.如果你想增加代碼來以某種方式調整你的文本-比如,在繪製框中居中你的文本,你可能會使用經典的居中算法.-用窗體的寬度減去要繪製的文本的寬度,然後除以2,告訴你從距離窗體左邊多少像素開始繪製.

靜態圖標控件稍微難一點兒.實際上,"靜態圖標控件"這個術語有些歧義,假定我們想要我們的圖標控件可以表現動畫的話.即使如此,開發這些圖標控件也不難,假設你已經有了豐富的精靈庫來處理所有開發動畫的細節:檢測兩幀之間的時間差,使用這個差值來判斷你的精靈將要走多少幀,等等.

圖標控件只有當你在每一幀並不繪製整個GUI系統的時候才變得麻煩.這種情況下,你多少要處理一些圖標控件的裁剪工作,這樣即使每幀都繪製,也不會覆蓋屬於在其上的窗口的像素(但是沒有改變,所以沒有繪製).我沒有開發這個-我的GUI每一幀都重畫-但是如果你面臨這個問題,你可能會想試試爲每個圖標設立裁剪列表,用它來繪製圖標,當有任何一個窗體移動、關閉或者打開時重新計算它.這或許是個可行的方法-我只是如此構想-但是這起碼是一個好的切入點.

框架控件也很簡單.我開發我的框架控件時只是圍繞m_position繪製邊框,然後在大約繪製座標(5,5)點附近(大約從框架控件的左上角向右向下5個像素)繪製窗口標題,你可以依照自己的想象自己決定.

你在開發靜態控件中可能碰到的麻煩事是稍微改變findwindow函數的功能以使它跳過所有的靜態控件窗口.這樣,如果一個靜態文本控件是在一個按鈕之上的,用戶可以透過靜態控件來按這個按鈕.當開發"簡易移動"窗口(即你可以通過按住窗口的任何部位來移動窗口,而不僅僅是標題欄,就象winamp)的時候,這很有用.

現在讓我們來看看如何開發按鈕.

按鈕控件

按鈕只比靜態控件難一點兒.你的按鈕控件需要不斷跟蹤是否它被按下或鬆開.它通過兩個虛函數來實現,wm_mousedown()和wm_mouseup(),你的calcall()函數需要在適當的時候調用它們.

基本上,在wm_mousedown()裏,你要設定一個布爾變量,我把它叫做"depressed flag"(按下標誌)爲真,而在wm_mouseup()裏,把它設爲假.然後再你的繪製代碼裏如果按下標誌爲真,繪製按鈕的按下狀態,否則,繪製鬆開狀態.

然後,增加一個附加狀態-即"只有當按下標誌爲真和鼠標指針在繪製區域之中時繪製按鈕的按下狀態,否則把按下標誌設爲假."如果你把鼠標移出按鈕這將使你的按鈕彈起,並且對於精確判斷一個按鈕何時被按下非常重要.

對於普通的GUI,當一個按鈕被點擊,將爲他的父窗體引發一個事件,窗體會做按鈕所代表的任何事-例如,點擊關閉按鈕將關閉窗口,點擊存儲按鈕將存儲文件,等等.我的GUI在且僅在wm_mouseup()中判斷按鈕是否被點擊,按下標誌是否爲真.按下標誌在mouseup()中還爲真的唯一情況是用戶在鼠標在按鈕之內按下和鬆開鼠標鍵.這允許用戶在最後放棄選擇-通過保持鼠標鍵按下並把鼠標指針拖到按鈕之外鬆開,就象其他的GUI一樣.

這就是按鈕了.現在來看看文本框吧.

插入符和文本控件

我選擇的是非常簡單的文本控件.它僅僅捕捉擊鍵,而且還不卷屏-但是你可能會要更加複雜的,也就是一個可以精確處理跳到開始(home)、跳到末尾(end)、插入和刪除字符,或者可能還要通過windows剪貼板支持剪切、拷貝、粘貼.

但是在我們做文本框之前,我們需要一個插入符.如果你對這個術語不熟悉,這裏解釋一下.插入符是光標的另一種說法-是的,就是那個小小的閃動的豎線.插入符告訴用戶他們的擊鍵將會在哪裏出現文字.

從我的GUI考慮,我很簡單的處理這些事-我指定活動窗口是具有插入符和句號(這裏rick不是很明白)的窗口.大多數GUI都是這樣的,好像也是最好的解決辦法.而且我的GUI象windows那樣把文本框的標題(caption)當作文本框裏的文字來處理.

那麼你怎麼開發插入符呢?好的,我想因爲我們知道插入符總是在活動窗口裏被繪製,並且插入符只有在活動窗口是文本框的時候出現,很容易聯想到插入符繪製代碼是文本框的一部分並且在文本框的繪製函數裏完成.這就使它很易於開發-只要用一個整形變量來代表窗口標題字符數組的索引,你的文本框就有要繪製插入符的所有信息了.

這就基本上表示,如果是個文本框的話,你要做的所有繪製工作就是圍繞繪製區域畫邊線,在邊線之內繪製窗口標題,然後如果是活動窗口,在正確的位置畫出插入符.在我的GUI裏,文本框中字符的最大長度是由文本框窗口的大小來決定的,也就是說我不用處理在文本框之內滾動文字.然而你或許會想要用戶可以在很小的文本框裏輸入很長的字串並可以滾動查看文本框中的內容.

現在來看看關於文本框的最難的東西-鍵盤處理.一旦會有擊鍵發生,很容易建立一個wm_keypressed()虛函數並且調用它,同樣很容易爲wm_keypressed開發文本框處理器,然後要麼把字符放到窗口標題的末尾,要麼處理特殊擊鍵(backspace鍵,等等-這是你的字串類要關注的東西),然後移動插入符.

難的地方在於在第一位置得到擊鍵.windows提供了至少三種完全不同的方法來查詢鍵盤-WM_KEYDOWN事件,GetKeyboardState()和GetAsyncKeyState()函數,當然還有DirectInput.我使用了DirectInput方法,這是因爲我在開發鼠標光標的時候就已經作了大量的和DirectInput相關的工作,另外通過DirectInput來獲取鍵盤狀態對我也是最簡潔和優雅的方法.

要使用DirectInput的鍵盤函數,你要做的第一件事是建立鍵盤設備.這和我們在第一章中建立DirectInput的鼠標設備的方法令人難以相信的相似.基本上,唯一的差別在於不是告訴DirectInput把我們的新設備當作鼠標來處理,而是當作鍵盤.如果你已經瞭解DirectInput處理鼠標的方法,那麼再把同樣的事情爲鍵盤再做一遍.

一旦獲取了鍵盤設備我們就可以查詢它.

要實際判斷一個鍵是否被按下需要多一點工作.基本上,要判斷哪個鍵被按下,你需要對所有101個鍵的狀態的兩個快照-一個來自上一幀另一個當前幀.當前幀中被按下的而上一幀沒有按下的鍵是被"點擊"的,你要爲他們發送wm_keypressed()消息.

來看看進度條?

進度條

進度條如同靜態控件一樣易於開發,因爲他們只接收很少幾個消息.

基本上,你需要爲進度條做兩件事-你要告訴它最大/最小範圍和步長.例如,我要創建一個載入進度條,由於我要載入100個不同的遊戲資源.我會創建一個範圍爲0到100的進度條.我會把進度條初始爲0,然後,當我載入一個資源的時候我會用單位長度來讓進度條前進一個步長.當進度條前進時,它都會重畫自身,圖形上用一個和繪製區成比例的長條來表示出它有多長.

進度條很象滾動條;實際上,可以用滾動條的方法來開發進度條.我把進度條和滾動條分開開發是因爲我想要他們有非常不同的外觀和細微差別的行爲-你的需要可能會不同.

滑動條和滾動條

繪製滑動條或者滾動條和繪製進度條很相似,這表現在你需要用滑動條的繪製矩形的百分比,它提供了繪製滑快的位置信息,來表現它的當前位置.你要爲垂直和水平控件作些細微的修改-我先做了個基類,gui_slider,其中包含了所有的公用代碼和所有的成員變量,然後開發兩個不同的派生類,gui_slider_horz和gui_slider_vert,它們處理繪製和點擊邏輯的不同.

就象處理鼠標點擊一樣,我爲滑動條選擇了簡便的方法.如果鼠標點擊在滾動條繪製區內發生,直接自動地滾動到那個位置.在我的滑動條裏,你不能同時在軸上點擊和移動位置-直接跳到你點擊的地方.我這麼做主要是因爲這樣會很簡單,而且我不喜歡windows默認的方法.

關於滾動條/滑動條的邏輯,你知道和進度條的基本設定是一樣的-最小、最大、當前位置.然而不象進度條,用戶可以通過在控件上點擊改變當前位置.

現在看看滾動條.我的GUI裏滾動條就是有兩邊各有一個按鈕的滑動條.這兩個按鈕(上/下或左/右箭頭)會移動滑快單位距離.這種方法消除了大量的按鈕類和滾動條之間的代碼複製,我強烈推薦你看看做相似的事.

看完了滾動條,看看最複雜的控件吧.

列表框控件

移出精力看這個吧,列表框控件是你要花最多時間的地方.

// represents a column in our listbox
class gui_listbox_column
{
public:
    gui_listbox_column() { }
    virtual ~gui_listbox_column() { }

    virtual void draw(uti_rectangle &where);

    void setname(const char *name) { m_name = name; }
    uti_string getname(void) { return(m_name); }

    int getwidth(void) { return(m_width); }
    void setwidth(int w) { m_width = w; }

private:
    uti_string m_name;
    int m_width;
};

// an item in our listbox
class gui_listbox_item
{
public:
    gui_listbox_item() { m_isselected = 0; m_indent = 0; }
    virtual ~gui_listbox_item() { }

    virtual draw(int colnum, uti_rectangle &where);

    void clearallcolumns(void); // boring
    void setindent(int i) { m_indent = i; }
    int getindent(void) { return(m_indent); }

    void settext(int colnum, const char *text); // boring
    uti_string gettext(int colnum = 0); // boring

    void setitemdata(unsigned long itemdata) { m_itemdata = itemdata; }
    unsigned long getitemdata(void) { return(m_itemdata); }

    void setselected(int s = 1) { m_isselected = s; }
    int getselected(void) { return(m_isselected); }

private:
    int m_isselected;
    int m_indent; // # of pixels to indent this item
    unsigned long m_itemdata;
    uti_pointerarray m_coltext;
};

// the listbox itself
class gui_fancylistbox : public gui_window
{
public:
    gui_fancylistbox() { m_multiselect = 0; }
    virtual ~gui_fancylistbox() { clear(); }

    int getselected(int iter = 0);

    virtual int wm_command(gui_window *win, int cmd, int param);
    virtual int wm_paint(coord x, coord y);
    virtual int wm_lbuttondown(coord x, coord y);

    gui_scrollbar_horz &gethscroll(void) { return(m_hscroll); }
    gui_scrollbar_vert &getvscroll(void) { return(m_vscroll); }

    virtual int wm_sizechanged(void); // the window's size has changed somehow

    gui_listbox_item *getitemat(int index); // boring
    gui_listbox_item *additem(const char *text); // boring
    int delitem(int index); // boring
    int delallitems(void); // boring
    gui_listbox_column *getcolumn(int index); // boring
    int addcolumn(const char *name, int width); // boring
    gui_listbox_column *getcolumnat(int index); // boring
    int delcolumn(int index); // boring
    int delallcolumns(void); // boring

    int clear(void); // delete columns & items

    int getnumitems(void);
    int getnumcols(void);

    void deselectall(void);
    void selectitem(int item);
    void selecttoggleitem(int item);

    void deselitem(int item);

private:
    int m_numdispobjsy;
    int m_vertgutterwidth; // # of pixels between items vertically

    gui_scrollbar_horz m_hscroll;
    gui_scrollbar_vert m_vscroll;

    bool m_multiselect; // is this multi-selectable?
    uti_pointerarray m_items; // array of gui_listbox_items
    uti_pointerarray m_columns; // array of gui_listbox_columns
};

列表框是到現在爲止你做的最難的控件吧?但這僅僅是因爲它是最通用的.一個能夠處理多列、縮進、多重選擇列表框控件將在實踐中證明他對你的遊戲是不可或缺的.停下來並想想在大多數遊戲裏用到列表框的地方,你就會很快發現這一點.

我把我的列表框控件分成兩部分:一個多列的"報表風格"的列表控件和一個圖標列表控件,它創建一個類似於當你在windows"我的電腦"裏選擇大圖標察看方式的顯示.

圖表列表控件比較容易建立.它使用了一列靜態圖標(在一次代碼重用),所有的具有相同的大小.我使用圖標的寬度除列表框的寬,這讓我知道有幾列可用.(如果證明我的列表框比大圖表小,我假設我只有一列,並讓繪製系統剪裁圖標以使他們不會超出我的繪製區域).一旦我有了列數,我通過圖標的總數除以它計算出我所需要的行數.這樣我就知道我該怎樣設定要包括的滾動條.

注意當控件改變大小時必須重新計算這些值.爲此我設定了一個wm_sizechanged()消息,calcall()將會在窗口繪製區域被改變的時候調用它.

報表風格列表控件要複雜一些.我先寫了兩個輔助類,gui_listbox_column和gui_listbox_item,它們包含了所有的關於列表中給定物件和列的信息.

gui_listbox_column是兩者中較簡單的.主要的列表框類有一個成員變量身份的gui_listbox_column的動態數組,這代表了目前列表框中的列.gui_listbox_column包含了在列表框中所需要的列的所有信息,包括列的名字,列的對齊,顯示或隱藏,大小等等.

主要的列表框類也有一個gui_listbox_item的動態數.gui_listbox_item類包含了與我們的報表風格列表框中特定行(或物件)相關的所有信息.目前這個類最重要的數據成員是代表每列數據的字串數組.我也讓每個物件通過m_itemdata成員存儲一個附加的32位數據.這個技術類似於windows允許你通過位你的列表物件調用SetItemData()和GetItemData()來存儲32位數據.這個細節很重要,因爲它允許列表框的用戶爲每個物件存儲一個指針-通常一個與該物件有關的特定類,以使它以後可用.

怎麼繪製列和物件呢?我傾向於在要繪製的列表框中在每個單獨的物件/列上有個絕對的控件.到最後,我決定讓列表控件通過不斷調用兩個虛函數,gui_listbox_item::draw()和gui_listbox_column::draw()來繪製他的物件和列.每個函數使用一個代表列或者物件在屏幕上位置的矩形.默認的對這些draw()函數的開發僅僅分劃出與矩形中特定列和子物件相關的文本;然而,我先在可以簡單的爲需要獨特外觀的物件或列派生和重載draw().這種技術目前工作的很好,但是我還不足以宣稱這是最好的方法.

然而,繪製物件比行需要更多的工作.物件需要用高光繪製,這決定於他們是否被選擇.這並不很難,但一定不能忘記.

然後就是滾動條的問題了.我的列表框包含兩個成員,m_horzscrollbar和m_vertscrollbar,他們都是GUI滾動條.當列表框的大小被改變時(wm_sizechanged()),他會看看數據的寬度和高度並決定是否顯示滾動條.

總結

真是繞了一大圈子,但是幸運的是你對爲GUI創建控件有了個大致的想法.這裏我想強調的是"風格就是樂趣".在做你的GUI時不要害怕創新-做做你曾經夢想過的東西,和使你的遊戲最酷的東西.如果你的遊戲很依賴你的GUI的效能這一點尤其重要,比如你在作即時戰略遊戲.

還要記住當創建控件的時候,你需要爲你的遊戲的其他部分考慮平衡問題-細節表現力和開發時間是成正比的.儘量給你的玩家最易於上手的GUI,但同時不要花時間做50種不同的控件.你要在功能、好事、複雜性、壞事中做出平衡.

控件就到這吧.下一章也是最後一章中我們會看看資源編輯器,序列化窗口和創建對話框.祝愉快!
 
 
 

發佈了35 篇原創文章 · 獲贊 1 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章