?
  看起來我在做發明車輪的浪費時間的事-- windows已經有一套非常復雜功能完善的GUI.不幸的是windows的GUI適用于辦公軟件,而這通常不適合游戲軟件.游戲一般需要比 windows更精確的控制(例如,使用自己的GUI實現用alpha混合創造的部分透明的窗口比較容易,而使用windows的GUI則幾乎做不到).

這篇文章將講述如何使用C++和DirectX來創建自己的GUI.文章分為幾部分,每部分涉及一個特定的GUI編程主題.這并不需要按順序閱讀,所以你可以從你需要的部分開始.

這里我假設讀者有以下的背景知識:事件驅動的編程原理;對PDL和C++的熟練掌握.我使用C++來建立我的GUI系統,因為我是C++愛好者俱樂部的持卡會員,并且C++的OOP很適用于windows編程和控制方式.

讓我們從定義工作范圍開始. 應該認識到我們不是要設計windows95,我們只是想要為一個游戲開發一套簡單的GUI.所以我們不用實現每一個簡單控制和GUI結構.我們只需要幾 個部分而已:一個鼠標指針,一個普通窗口,一些在窗口中的對話框控制.我們也需要一個資源編輯器--一個允許我們在圖形環境中使用拖動控制來設計對話框.

從基礎開始:渲染循環

我將通過定義一個計算并繪制框線的函數開始.我們在RenderGUI()函數中調用這個函數.RenderGUI用PDL描述如下:

void CApplication::RenderGUI(void)
{

??
// get position and button status of mouse cursor
??
// calculate mouse cursor抯 effects on windows / send messages
??
// render all windows
??
// render mouse
??
// flip to screen
}

我們得到新的位置和鼠標光標的狀態,計算由新的數據引起的任何改變,渲染所有的窗口,鼠標指針并在屏幕上繪制.

鼠標

class CMouse {
public:

?
CMouse(); // boring
?
~CMouse(); // boring

?
int Init(LPDIRECTINPUT di); // we抣l talk about this later
?
int Refresh(void); // we抣l talk about this later

?
int GetButton(int index)
?
{
???
if (index < 0 || index > NUMMOUSEBUTTONS) return(0);
???
return(m_button[index]);
?
}

?
void clear(void); // sets all vars to zero?

? // makes sure p is a valid on-screen point
?
void ConstrainPosToScreenSize(CPoint &p);
?
CPoint GetAbsPosition(void) { return(m_absposition); }
?
CPoint GetRelPosition(void) { return(m_relposition); }
?
enum { NUMMOUSEBUTTONS = 3 }; // three button mouse

private:
?
LPDIRECTINPUTDEVICE m_mousedev;
?
char m_button[NUMMOUSEBUTTONS]; // state of buttons
?
CPoint m_absposition; // actual screen position
?
CPoint m_relposition; // relative position

};

絕對位置和相對位置,DirectInput

為什么我要使用DirectInput? 這實際是一個口味問題.有兩種方法可得到windows的鼠標信息,從DirectInput中或通過一條叫做GetCursorPos()的Win32 函數.主要的區別在于DirectInput提供的是相對坐標,即相對于上次位置的當前位置;而GetCursorPos()將提供屏幕坐標系的絕對坐 標.絕對坐標對GUI很有用;而相對坐標則適合于沒有光標的鼠標,例如在FPS游戲中的環視(譯者:即光標位置固定的情況).然而你可以由絕對坐標計算出 相對坐標,反之亦然.

由于很多原因我選擇了DirectInput,所有這些原因超出了本文的范圍.對你來說GetCursorPos()可能是個更好的選擇?如果是這樣,鼠標類可能就會非常龐大.DirectInput更加技巧化(也更有趣),所以本文將集中于DirectInput.

初始化DirectInput

在我們詳述CMouse 之前,讓我們看看如何初始化DirectInput.注意這些代碼并不屬于我們的CMouse::Init()函數;DirectInput的指針在整個 游戲中都會使用,而并不僅僅在鼠標類中,所以DirectInput的初始化應在主初始化函數中和DirectDraw,DirectSound的初始化 同時完成.DirectInput指針和DirectInput device指針不同;DirectInput指針用以得到DirectInput device指針.注意理解這一區別,下面是初始化主DirectInput接口指針的代碼

LPDIRECTINPUT di = NULL;

hr = DirectInputCreate(hinst, DIRECTINPUT_VERSION, &di, NULL);

if (FAILED(hr)) {
?
// error
?
handle_error ();
?
}

// Now that we抳e got a DirectInput interface, let抯 begin
// fleshing out our CMouse by implementing CMouse::Init().?

bool CMouse::Init(LPDIRECTINPUT di)
{

?
// Obtain an interface to the system mouse device.
?
hr = di->CreateDevice(GUID_SysMouse, (LPDIRECTINPUTDEVICE*)&m_mousedev, NULL);?

? if (FAILED(hr)) { /* handle errors! */ }
?
//? Set the data format to "mouse format".
?
hr = m_mousedev->SetDataFormat(&c_dfDIMouse);
?
if (FAILED(hr)) { /* handle errors! */ }

?
// Set the cooperativity level
?
hr = m_mousedev->SetCooperativeLevel(hwnd, DISCL_NONEXCLUSIVE | DISCL_FOREGROUND);
?
if (FAILED(hr)) { /* handle errors! */ }
}

這段代碼作了三件重要工作. 第一,得到一個有效的DirectInput鼠標設備接口,并把他付給di_mouse.第二,設定數據格式和設備的協作度,僅僅讓windows知道我 們想查詢鼠標設備,而不是要獨占該設備.(獨占意味著只有我們的應用程序可以使用鼠標,通過指定DISCL_NONEXCLUSIVE,我們通知 windows我們將和其他程序共享鼠標)

通過DirectInput檢測鼠標狀態

現在讓我們關注CMouse::Refresh().該函數負責更新CMouse的內部鍵狀態和位置.下面是代碼:

void CMouse::Refresh(void)
{

?
char done = 0;
?
int q;
?
HRESULT hr;
?
CPoint p;
?
DIMOUSESTATE dims;?

? // clear our struct ?eventually, directinput will fill this in
?
memset(&dims, 0, sizeof(DIMOUSESTATE));

?
if (!m_mousedev) return; // we don抰 have a pointer! Bail!

? while (!done)
? {
??? // query DirectInput for newest mouse data
??? hr = m_mousedev->GetDeviceState(sizeof(DIMOUSESTATE), &dims);
??? if (FAILED(hr))
??? {
????? if (hr == DIERR_INPUTLOST || hr == DIERR_NOTACQUIRED)
????? {
??????? // device lost... reacquire
??????? hr = m_mousedev->Acquire();
??????? if (FAILED(hr))
??????? {
????????? // error handling goes here
????????? clear();
????????? done=1;
??????? }
????? }
????? else
????? {
????????? // it抯 some other error ?handle it
????????? clear();
????????? done = 1;
????? }
??? }
??? else // read mouse successfully!
??? {
????? done = 1;
??? }
? } // end while loop ?we抳e read DI correctly

? // squirrel away newest rel position data
? m_relposition.x = dims.lX;
? m_relposition.y = dims.lY;
? m_relposition.z = dims.lZ;

? // now calc abs position from new relative data
? m_absposition.z += dims.lZ;
? m_absposition.x += dims.lX;
? m_absposition.y += dims.lY;?

? // keep the mouse pointer on-screen...
? ConstrainPosToScreenSize(m_absposition);

? // get button data
? for (q=0; q < NUMMOUSEBUTTONS; q++)
? {
??? m_button[q] = (dims.rgbButtons[q] & 0x80));
? }

}

這段代碼作了許多工作. 首先,通過DirectInput得到新的絕對鼠標位置(使用while循環來自動重新檢測).其次,把絕對位置數據付給m_absposition,然 后得到新的相對位置.ConstrainPosToScreenSize()函數確保鼠標所在點在屏幕范圍內.最后,通過循環更新所有的鍵.

繪制鼠標

繪制鼠標光標有兩個主要的原則. 如果你知道每一幀整個屏幕都會用新的像素數據刷新,你可以簡單地blt鼠標光標到新的絕對位置,并由此完成繪制.然而,更好的方法是在你blt之前,得到 鼠標光標下的像素數據得一份拷貝,然后,當鼠標移動時,通過blt保存過的像素數據從而擦除舊的blt.我更傾向于后一種方法.

本文將不會討論具體的blt表面及其他,你應該懂得如何實現.

線程和尾跡

如果你不介意多線程,有一種更好的方法來解決鼠標問題.這里的方法是單一線程的,每一幀都會查詢鼠標.

適用于高刷新率,而不是低刷新率.鼠標光標會顯得很遲緩.最好的方法是建立獨立的mouse-rendering線程,它會不斷地檢測MOUSEMOVE消息,并當鼠標移動時刷新和blt鼠標光標.多線程的優勢在于不管你的游戲刷新率

是多少,鼠標光標都會很流暢.有一個單獨的鼠標線程將使游戲在任何幀頻率下都會相應很快.

建立鼠標軌跡對你顯然是件容易的事.在一個數組中保存最后幾個鼠標光標的位置.當鼠標移動時,丟棄最舊的坐標,把所有其他的坐標值向下移動一個欄位,并把新的坐標存入頂端的欄位.然后,如果你想要額外的效果.使用alpha混合,并以比新坐標更高的透明度渲染舊的坐標.

期待...

現在我們結束了鼠標光標的工作.下一節將講述如何創建基本的窗體,及如何移動窗體.敬請期待.

posted on 2006-04-22 23:03 我不是神 閱讀(77) 評論(4) ?編輯?收藏

評論

#?re: 使用C++和Directx開發GUI 2006-04-22 23:04 我不是神

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



歡迎您繼續閱讀"使用C++和Directx開發GUI"的第二部分.這里是第一部分.接著我們的主題(講解在我未來的游戲如何使用GUI(圖形 用戶界面)),本文將解釋窗體的許多神秘之處.我們將關注窗體樹如何工作,為我們使用GUI制訂計劃,以及創建窗體類的細節,包括繪制,消息機制,坐標系 統和其他所有的麻煩事兒. 在此我們將著重使用C++.如果你對純虛函數,dynamic_cast'ing等等已經生疏了,那么趕快翻翻C++書再繼續吧.

不開玩笑了,讓我們開始.

在涉及代碼之前,明確我們的目標是很重要的.

在我們的游戲已完成的GUI里,我們將使用一個樹來跟蹤顯示在屏幕上的每個窗體.窗體樹是個簡單的N節點樹.樹的根部是視窗桌面(windows desktop).桌面窗體(Desktop window)的子窗體通常是應用程序的主窗體;主窗體的子窗體是對話框,對話框的子窗體是獨立的對話控件(按鈕,文本框等).重要的區別在于--窗體的 外觀并不取決于它在樹中的位置.例如,許多游戲把按鈕直接放在他們的桌面窗體上,就如同對話框一樣. 是的,按鈕也是窗體.意識到這一點是很重要的.一個按鈕只是一個有著有趣外觀的窗體.實際上,所有的GUI控件都是有著不同外觀的簡單窗體.這體現了C+ +的能力.如果我們創建一個繼承的窗體類,給它幾條虛函數,我們就能通過重載基類的函數輕易地創建我們的控件.如此應用多態性簡直稱得上優雅;實際上,許 多C++書將它作為范例(在第三部分我將詳述此點). 這是我們的基本設計,下面讓我們想想應用方法.

計劃

當我應用我的GUI時,我做了如下幾步:

1.首先我寫了些基本的窗體管理代碼.這些代碼負責窗體樹,增加/刪除窗體,顯示/隱藏窗體,把它們移動到Z坐標的頂端(即在最前顯示),等等. 我通過在窗體應處的位置繪制矩形完成了窗體的繪制過程,然后根據窗體的Z坐標在左上角繪制一個數字. 如果你購買或編寫一個優秀可靠的指針陣列的模版類,那你的生活將會變得非常輕松.STL(標準模版庫Standard Template Library)得到許多C++版本的支持,它有很多好的模板性的指針陣列類,但是如果你想使用你自己的模板類,在你應用于你的窗體管理之前要進行完整徹 底的測試.現在你要注意的問題是由錯誤的陣列類所引起的不易察覺的內存泄漏或空指針引用.

2.一旦我有了基礎的窗體管理函數,我花了一些時間思考我的坐標系統.寫了一些坐標管理函數.

3.下一步,我處理窗體繪制代碼.我繼承一個"奇異窗體"類,并顯示它如何使用一套九個精靈程序繪制自身的--其中四個精靈程序繪制角落,四個繪 邊,一個繪制背景. 使用這九個窗體精靈程序,使創建既有獨特的藝術外觀又可動態改變大小(ala StarDock's WindowBlinds)的窗體成為可能.這樣做的基礎是你需要有一個相當智能的繪圖庫,一個能處理封存精靈程序,彈性精靈程序以及集中精靈程序的庫, 并且它是一個非常復雜的窗體生成程序(一些藝術家可以用以創建他們的窗體的代碼),這使這種方法可以實際的實現.當然,你也要注意窗體繪制速度.

4.一旦普通窗體的繪制代碼完成,我開始實現控制部分.代碼控制是簡單的,但還是需要非常徹底的測試.我由簡單的控制:靜態,圖標等開始像在前面解釋的那樣來回反復我的工作.

5.最后,完成我的控制部分后,我開始編寫一個簡單的資源編輯器,一個允許用戶可視的放置控件,布局對話框的程序.這個資源編輯器用了我整整一個 月的時間,但我強烈建議這樣做(而不是用文本文件去決定位置)--圖形化對話框的建立非常容易,并且這也是一個好的練習:在完善中我在我的控制部分的代碼 中沒有發現幾個bug,在實際的程序中被證明是很難解決的.

我被編寫一個可以轉換MSVC++的資源(.RC)文件為我的GUI可使用的資源文件的程序的這個想法困擾了好久.最后,我發現這樣一個程序遠比 它的價值麻煩.我寫這個GUI的目的就是要擺脫Windows的限制,為了正真的做到這一點,我要由自己的編輯器,使用我自己的資源文件格式,按自己的形 式做事情.我決定用MFC由底層實現一個所見即所得(WYSIWYG)的資源編輯器.我的需求,我決定;你的需求也許不同.如果某人想要寫一個轉化器,我 將很樂于聽到這樣的消息. 現在到哪了?這篇文章剩下的部分將探究開始的兩步.這一系列的第三部分將進入令人麻木的控制代碼細節.第四部分將討論一點資源編輯器的實現和序列化窗體. 因此...讓我們來開始第一步:基本的窗體管理代碼.

實現

我們開始.這是為我們基本窗體類定義的好的開始:

class gui_window
{
public:
gui_window(); // boring
~gui_window(); // boring
virtual void init(void); // boring
gui_window *getparent(void) { return(m_pParent); }

/////////////
// section I: window management controls
/////////////

int addwindow(gui_window *w);
int removewindow(gui_window *w);

void show(void) { m_bIsShown = true; }
void hide(void) { m_bIsShown = false; }
bool isshown(void) { return(m_bIsShown); }
void bringtotop(void);
bool isactive(void);

/////////////
// Section II: coordinates
/////////////

void setpos(coord x1, coord y1); // boring
void setsize(coord width, coord height); // boring

void screentoclient(coord &x, coord &y);

int virtxtopixels(coord virtx); // convert GUI units to actual pixels
int virtytopixels(coord virty); // ditto

virtual gui_window *findchildatcoord(coord x, coord y, int flags = 0);

/////////////
// Section III: Drawing Code
/////////////

// renders this window + all children recursively
int renderall(coord x, coord y, int drawme = 1);

gui_wincolor &getcurrentcolorset(void)
{ return(isactive() ? m_activecolors : m_inactivecolors); }

/////////////
// Messaging stuff to be discussed in later Parts
/////////////

int calcall(void);

virtual int wm_paint(coord x, coord y);
virtual int wm_rendermouse(coord x, coord y);
virtual int wm_lbuttondown(coord x, coord y);
virtual int wm_lbuttonup(coord x, coord y);
virtual int wm_ldrag(coord x, coord y);
virtual int wm_lclick(coord x, coord y);
virtual int wm_keydown(int key);
virtual int wm_command(gui_window *win, int cmd, int param) { return(0); };
virtual int wm_cansize(coord x, coord y);
virtual int wm_size(coord x, coord y, int cansize);
virtual int wm_sizechanged(void) { return(0); }
virtual int wm_update(int msdelta) { return(0); }

protected:

virtual void copy(gui_window &r); // deep copies one window to another

gui_window *m_pParent;
uti_pointerarray m_subwins;
uti_rectangle m_position;

// active and inactive colorsets
gui_wincolor m_activecolor;
gui_wincolor m_inactivecolor;

// window caption
uti_string m_caption;
};


當你細讀我們討論的函數,你將會發現遞歸到處可見.比如,我們的程序將通過調用一個源窗體的方法renderall()來繪制整個GUI系統,這 個方法又將回調它的子窗體的renderall()方法,這些子窗體的renderall()方法還要調它們的子窗體的renderall()方法,以此 類推.大部分的函數都遵循這種遞歸模式. 整個GUI系統有一個全局的靜態變量--源窗體.出于安全性的考慮,我把它封裝在一個全局的函數GetDesktop()中.
現在,我們開始,我們來完成一些函數,由窗體管理代碼開始,如何?


窗體管理

/****************************************************************************
addwindow: adds a window to this window's subwin array
****************************************************************************/
int gui_window::addwindow(gui_window *w)
{
if (!w) return(-1);
// only add it if it isn't already in our window list.
if (m_subwins.find(w) == -1) m_subwins.add(w);
w->setparent(this);
return(0);
}

/****************************************************************************
removewindow: removes a window from this window's subwin array
****************************************************************************/
int gui_window::removewindow(gui_window *w)
{
w->setparent(NULL);
return(m_subwins.findandremove(w));
}

/****************************************************************************
bringtotop: bring this window to the top of the z-order. the top of the
z-order is the HIGHEST index in the subwin array.
****************************************************************************/
void gui_window::bringtotop(void)
{
if (m_parent) {
// we gotta save the old parent so we know who to add back to
gui_window *p = m_parent;
p->removewindow(this);
p->addwindow(this);
}
}
/****************************************************************************

isactive: returns true if this window is the active one (the one with input focus).
****************************************************************************/
bool gui_window::isactive(void)
{
if (!m_parent) return(1);
if (!m_parent->isactive()) return(0);
return(this == m_parent->m_subwins.getat(m_parent->m_subwins.getsize()-1));
}

這一系列函數是處理我所說的窗體管理:新建窗體,刪除窗體,顯示/隱藏窗體,改變它們Z坐標.所有的這些都是完全的列陣操作:在這里你的列陣類得 到測試. 在增加/刪除窗體函數中唯一感興趣的問題是:"誰來對窗體指針負責?"在C++中,這總是一個問自己得很好的問題.Addwindow和 removewindow都要獲得窗體類的指針.這就意味這創建一個新的窗體你的代碼新建一個指針并通過addwindow()把指針傳到父(桌面)窗 體.那么,誰來負責刪除你新建的指針呢?

我的回答是"GUI不擁有窗體指針;游戲本身負責增加指針".這與C++的笨拙規則"誰創建誰刪除"是一致的.

我選擇的可行的方法是"父窗體為它的所有子窗體指針負責".這就意味著為了防治內存泄漏,每個窗體必須在它的(虛擬)析構函數(記住,有繼承類)中搜尋它的子窗體列陣并且刪除所有的包括在其中的窗體.

如果你決定實現一個擁有指針系統的GUI,注意一個重要的原則--所有的窗體必須動態的分配.這樣的系統崩潰最快的方法是把一個變量的地址傳到堆 棧中,如調用"addwindow(&mywindow)",其中mywindow被定義為堆棧中的局部變量.系統將好好工作直到 mywindow超出它的有效區,或其父窗體的析構函數被調用,此時系統將試圖刪除給地址,這樣系統即崩潰.所以說"對待指針一定要特別的小心".

這就是為什么我的GUI不擁有窗體指針的主要原因.如果你在你的GUI中處理大量復雜的窗體指針(也就是說,比如你要處理屬性表),你將更想要這 樣一個系統,它不必跟蹤每一個指針比且刪除只意味著"這個指針現在為我所控制:只從你的列陣中移走它但并不刪除它".這樣只要你能保證在指針超出有效區前 removewindow(),你也可以使用(小心)在堆棧中的局部變量地址.

繼續?顯示和隱藏窗體通過一個布爾型變量來完成.Showwindow()和hindewindow()只是簡單的設置或清除這個變量:窗體繪制程序和消息處理程序在它們處理任何之前先檢查這個"窗體可見"標志位.非常簡單吧!

Z坐標順序也是相當的簡單.不熟悉這種說法,可把z坐標順序比為窗體"堆棧"一個重疊一個.一開始,你也許想像DirectDraw處理覆蓋那樣 實現z坐標順序,你也許決定給每個窗體一個整數來描述它在z坐標的絕對位置,也就是說,可能0表示屏幕的頂端,則-1000代表最后.我想了一下這種Z坐 標順序實現方法,但我不贊成--Z坐標絕對位置不是我所關心的;我更關心的是他們的相對位置.也就是說,我不需要準確的知道一個窗體在另一個的多后,我只 要簡單的知道這個給定的窗體在另一個的后面還是前面.

所以,我決定實現Z坐標順序如下:在列陣中有最大的索引值,m_subwins,的窗體在"最前".擁有[size-1]的窗體緊跟其后,緊接著 是[size-2],依次類推.位置為[0]的窗體將在最底.用這種方法Z坐標順序實現變得非常容易.而且,一舉兩得,我將把最前的窗體視為活動窗體,或 更技術的說法,它將被視為擁有輸入焦點的窗體.盡管我的GUI使用的這種"始終最前"窗體是有限制的(比如,在Windows NT中的任務管理器不管輸入焦點始終在所有的窗體之前),我覺得這樣有利于使代碼盡可能的簡單.

當然,我用數列表示Z坐標順序在我移動窗體到最前時處理數列付出了一些小的代價.比如,我要在50個窗體中將第二個窗體移到最前;我將為了移動二 號窗體而移動48個窗體.但信運的是,移動窗體到Z坐標最前不是最耗時的函數,即使是,也有很多好的快的方法可以處理,比如鏈表即可.

看看我在bringtotop()函數中的小技巧.因為我知道窗體不擁有指針,我就刪除這個窗體又馬上創建一個,非常有效率的將它重定位在數列最 前.我這樣做是因為我的指針類,uti_pointerarray,已經被編寫好了一旦刪除一個元素,所有的更高的元素將向后移動.

這就是窗體管理了.現在,進入有趣的坐標系統?

坐標系統

/****************************************************************************
virtual coordinate system to graphics card resolution converters
****************************************************************************/
const double GUI_SCALEX = 10000.0;
const double GUI_SCALEY = 10000.0;

int gui_window::virtxtopixels(int virtx)
{
int width = (m_parent) ? m_parent->getpos().getwidth() : getscreendims().getwidth();
return((int)((double)virtx*(double)width/GUI_SCALEX));
}

int gui_window::virtytopixels(int virty)
{
int height = (m_parent) ? m_parent->getpos().getheight() : getscreendims().getheight();
return((int)((double)virty*(double)height/GUI_SCALEY));
}

/****************************************************************************
findchildatcoord: returns the top-most child window at coord (x,y);
recursive.
****************************************************************************/
gui_window *gui_window::findchildatcoord(coord x, coord y, int flags)
{
for (int q = m_subwins.getsize()-1; q >= 0; q--)
{
gui_window *ww = (gui_window *)m_subwins.getat(q);
if (ww)
{
gui_window *found = ww->findchildatcoord(x-m_position.getx1(), y-m_position.gety1(), flags);
if (found) return(found);
}
}

// check to see if this window itself is at the coord - this breaks the recursion
if (!getinvisible() && m_position.ispointin(x,y))
return(this);
return(NULL);
}

我的GUI最大的優勢是獨立的解決方案,我稱之為"彈性對話框".基本上,我希望我的窗體和對話框根據它們運行系統的屏幕設置決定它們的大小.對 系統的更高的要求是,我希望窗體,控件等在640 x 480的屏幕上擴張或縮小.同時我也希望不管它們父窗體的大小,它們都可以適合.

這就意味著我需要實現一個像微軟窗體一樣的虛擬坐標系統.我以一個任意的數據定義我的虛擬坐標系統--或者說,"從現在起,我將不管窗體的實際尺 寸假設每一個窗體都是10000 x 10000個單元",然后我的GUI將在這套坐標下工作.對于桌面,坐標將對應顯示器的物理尺寸.

我通過以下四個函數實現我的想法:virtxtopixels(),virtytopixels(), pixelstovirtx(), 和pixelstovirty(). (注意:在代碼中之列出了兩個;我估計你已理解這個想法了).這些函數負責把虛擬的10000 x 10000單元坐標要么轉換為父窗體的真實尺寸要么轉換為顯示器的物理坐標.顯然,顯示窗體的函數將倚重它們.

函數screentoclient()負責取得屏幕的絕對位置并將它轉換為相對的虛擬坐標.相對的坐標從窗體的左上角開始,這和3D空間的想法是相同的.相對坐標對對話框是必不可少的.

在GUI系統中所有的坐標都是相對于其他的某物的.唯一的一個例外就是桌面窗體,它的坐標是絕對的.相對的方法可以保證當父窗體移動時它的子窗體 也跟著移動,而且可以保證當用戶拖動對話框到不同位置時其結構是一致的.同時,因為我們整個虛擬坐標系統都是相對的,當用戶拉伸或縮小一個對話框時其中的 所有控件都會隨之變化,自動的盡量適合新的尺寸.對我們這些曾在win32中試過相同特性的人來說,這是個令人驚異的特點.

最后,函數findchildatcoord()取得(虛擬)坐標確定哪個(如果有)子窗體在當前坐標--非常有用,比如,當鼠標單擊時,我們需 要知道哪個窗體處理鼠標單擊事件.這個函數通過反向搜尋子窗體列陣(記住,最前的窗體在列真的最后面),看那個點在哪個窗體的矩形中.標志參數提供了更多 的條件去判斷點擊是否發生;比如,當我們開始實現控制時,我們會意識到不讓標示和光標控件響應單擊是有用的,取而帶之應給在它們下面的窗體一個機會響應- -如果一個標示放在一個按鈕上面,即使用戶單擊標示仍表示單擊按鈕.標志參數控制著這些特例.



現在,我們已經有了坐標,我們可以開始繪制我們的窗體了?

繪制窗體

遞歸是一柄雙刃劍.它使得繪制窗體的代碼很容易跟蹤,但是它也會造成重復繪制像素,而這將嚴重的影響性能。(這就是說,例如你有一個存放50個相 同大小相同位置的窗體,程序會一直跑完50個循環,每個像素都會被走上50遍)。這是個臭名昭著的問題。肯定有裁剪算法針對這種情況,實際上,這是個我需 要花些時間的領域。在我自己的程序-Quaternion's GUI 在非游戲屏幕過程(列標題和關閉等等)中一般是激活狀態的,要放在對GUI而言最精確的位置是很蠢的想法,因為根本就沒有任何其他的動作在進行。

但是,我在對它進行修補。現在我試圖在我的繪制方法中利用DirectDrawClipper對象。到現在為止,初始的代碼看起來很有希望。下面 是它的工作方式:桌面窗口“清除”裁剪對象。然后每個窗口繪制它的子窗口,先畫頂端的,在畫底端的。當每個窗口繪制完畢后,把它的屏幕矩形加入到裁剪器, 有效地從它之下的窗口中“排除”這個區域(這假設所有的窗口都是100%不透光的).這有助于確保起碼每個像素將被只繪制一次;當然,程序還是被所有的 GUI渲染所需要的計算和調用搞的亂糟糟的,(并且裁剪器可能已經滿負載工作了),但是起碼程序不會繪制多余的像素.裁剪器對象運行的快慢與否使得這是否 值得還不明了。

我也在嘗試其他的幾個主意-也許利用3D顯卡的內建Z緩沖,或者某種復雜的矩形創建器(dirty rectangle setup).如果你有什么意見,請告訴我;或者自己嘗試并告訴我你的發現。

我剪掉了大量的窗體繪制代碼,因為這些代碼是這對我的情況的(它調用了我自定的精靈類).一旦你知曉你要繪制窗體的確切的屏幕維數(screen dimensions)時,實際的繪制代碼就能夠直接被利用。基本上,我的繪制代碼用了9個精靈-角落4個,邊緣4個,背景1個-并用這些精靈繪制窗體.

色彩集需要一點兒解釋.我決定每個窗口有兩套獨特的色彩集;一套當窗口激活時使用,一套不激活時使用.在繪制代碼開始之前,調用 getappropriatecolorset(),這個函數根據窗口的激活狀態返回正確的色彩集.具有針對激活和非激活狀態的不同色彩的窗口是GUI設 計的基本規則;它也比較容易使用.


現在我們的窗口已經畫完了,開始看看消息吧。

窗口消息

這一節是執行GUI的核心。窗口消息是當用戶執行特定操作(點擊鼠標,移動鼠標,擊鍵等等)時發送給窗口的事件.某些消息(例如 WM_KEYDOWN)是發給激活窗口的,一些(WM_MOUSEMOVE)是發給鼠標移動其上的窗口,還有一些(WM_UPDATE)總是發給桌面的.

微軟的Windows有個消息隊列.我的GUI則沒有-當calcall()計算出需要給窗口送消息時,它在此停下并且發送消息-它為窗口調用適 當的WM_XXXX()虛函數.我發現這種方法對于簡單的GUI是合適的.除非你有很好的理由,不要使用一個太復雜的消息隊列,在其中存儲和使用線程獲取 和發送消息.對大多說的游戲GUI而言,它并不值得.

此外,注意WM_XXXX()都是虛函數.這將使C++的多態性為我們服務.需要改變某些形式的窗口(或者控件,比如按鈕),處理鼠標左鍵剛剛被 按下的事件?很簡單,從基類派生出一個類并重載它的wm_lbuttondown()方法.系統會在恰當的時候自動調用派生類的方法;這體現了C++的力 量.

就我自己的意愿,我不能太深入calcall()的細節,這個函數得到所有的輸入設備并發出消息.它做很多事,并有很多對我的GUI而言特定的行 為.例如,你或許想讓你的GUI像X-Window一樣運行,在鼠標活動范圍之內的窗口總是處于激活狀態的窗口.或者,你想要使得激活窗口成為系統模態窗 口(指不可能發生其他的事直到用戶關閉它),就像許多基于蘋果平臺(Mac)的程序那樣.你會想要在窗口內的任何位置點擊來關閉窗口,而不是僅僅在標題 欄,像winamp那樣.calcall()的執行結果根據你想要GUI完成什么樣的功能而會有很大的不同.

我會給你一個提示,雖然-calcall()函數不是沒有狀態的,實際上,你的calcall()函數可能會變成一個很復雜的狀態機(state machine).關于這一點的例子是拖放物體.為了恰當的計算普通的"鼠標鍵釋放"事件和相似的但完全不同的"用戶在拖動的物體剛剛放下"事件之間的不 同,calcall()必須有一個狀態參數.如果你對有限狀態機已經生疏了,那么在你執行calcall()之前復習復習將會使你不那么頭痛.

在窗口頭文件中包括的wm_xxxx()函數是我感覺代表了一個GUI要計算和發送的信息的最小集合.你的需要可能會不同,你也不必拘泥于微軟視窗的消息集合;如果自定的消息對你很合適,那么就自己做一個.



窗口消息

在文章的第一部分我提到了一個叫做CApplication::RenderGUI()的函數,它是在計算之后繪制我們的GUI的主函數:

void CApplication::RenderGUI(void)
{
// get position and button status of mouse cursor
// calculate mouse cursor's effects on windows / send messages
// render all windows
// render mouse
// flip to screen
}


最后,讓我們開始加入一些PDL(頁面描述語言).

void CApplication::RenderGUI(void)
{
// get position and button status of mouse cursor
m_mouse.Refresh();

// calculate mouse cursor's effects on windows / send messages
GetDesktop()->calcall();

// render all windows
GetDesktop()->renderall();

// render mouse
m_mouse.Render();

// flip to screen
GetBackBuffer()->Flip();
}

查看這些代碼將會使你看到程序是如何開始在一起工作的.


在下一章,第三部分中,我們會處理對話框控件.按鈕,文本框和進度條.

參看使用C++和Directx開發GUI(一)



#?re: 使用C++和Directx開發GUI 2006-04-22 23:05 我不是神


使用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種不同的控件.你要在功能、好事、復雜性、壞事中做出平衡.

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

參看使用C++和Directx開發GUI(一)
參看使用C++和Directx開發GUI(二)



#?re: 使用C++和Directx開發GUI 2006-04-22 23:06 我不是神


使用C++和DirectX開發GUI - 資源編輯器及其它



  歡迎回到“使用C++和DX開發GUI”的第四部分。接著我們的主題(我是如何為我未來的游戲開發GUI的 — Quaternion),本文將關注所有的有關游戲GUI的細節問題。

4.1、保存窗口
  窗口序列化(存儲和載入窗口)對你的工程而言或許不重要。如果你的游戲GUI很簡單,你可以全靠程序在游戲中實現窗口。但如果你的GUI相對 復雜,或者隨著開發的過程經常會改變,那么你會想寫程序以把一個窗口(和所有它的子窗口)存到文件里,然后再裝載它。對初學者而言,窗口序列化代碼允許你 改變游戲的GUI而不用重新編譯,而且它對于多人協調工作也是有益的。
  我的計劃是從主對話窗口開始,然后遍歷它的所有子窗口,把每一個存到磁盤上。如果我用C語言寫程序的話,我告訴自己的第一句話是“好的,如 果我必須保存這些窗口,我需要每個窗口有一個字節來告訴我它是什么樣的窗口,以使我能正確的載入它。比如1是按鈕,2是列表框,3是圖表,等等。”
  這種問題是C++的RTTI(運行期類型識別)關心的。RTTI提供了兩個東東,type_info類和typeid()函數,兩者允許我 查詢一個對象的所屬類名稱,例如gui_window,gui_button等等。我沒有使用枚舉和ID,而是簡單地為每個將要保存的窗口調用typid (),并“寫下”窗口的類名稱。
  我注意到使用RTTI的對象識別函數來幫助保存窗口的兩個小缺點。第一,RTTI ID是字符串而不是整型,這意味著它們將占用磁盤上的更多空間(按照Pascal方式存儲字串將是前4個字節代表字串的長度,接下來是字串本身的數據)。 第二,如果你改變一個窗口類的名字,你會破壞已經存好的所有窗口文件。
  由于這些原因,你可能不會這樣使用RTTI。畢竟,并不是有技術就一定要使用它。然而,我發現RTTI對我的代碼而言卻是救生員。要獲得更多的關于RTTI和這兩個函數的信息,請在你的聯機幫助文檔里查找。
  另外,如果你決定在VC++里使用RTTI,確保你在工程屬性的C/C++欄和C++語言選項中打開它。

4.2、載入窗口
  載入窗口比存儲他們要難一點兒,這主要是因為你必須新建(new)每個窗口,載入,并當它不再需要的時候刪除。
  這是個遞歸函數,用PDL表達的話如下所示:

  void gui_window:load(int filehandle)
  {
    // read window properties (colorsets, etc.)
    // read total number of children for this window
    // for each child?
      // read window ID from disk
      // new a gui_window derivative based on that ID
      // tell the newly created window to load itself (recurse!)
    // next child
  }

  換句話說,你得像你所想得那樣從磁盤載入窗口。第一,要處理基類窗口:讀取他的屬性。然后,讀取基類窗口的所有子窗口的數目。對每個子窗口,讀取ID字節,根據ID新建一個窗口,然后讓新窗口載入自己(低軌到它)。當所有的字窗口載入完畢時,就結束了。
  當然,你的文件結構一致也非常重要。確保你的存儲代碼以你想要載入的順序存儲信息。

4.3、資源編輯器
  要想真正使你的GUI大放光彩,你必須有一個資源編輯器。當然你不需要做個象開發環境提供的資源編輯器那樣的華麗和強大,但是你起碼得有個基本的程序來完成加入,編輯,刪除,排列,免除你為對話框的每個控件計算虛擬坐標位置的麻煩事兒。
  寫一個功能完善的,所見即所得(WYSIWYG)的資源編輯器超出了本文的范圍,但是我能給你一些小提示來幫助你完成這個壯舉:

共享你的代碼。特別地,讓你的資源編輯器和你的游戲一起分享同樣的渲染代碼。這樣,你就得到了所見即所得支持,并且免除了開發兩套GUI代碼的麻 煩,我向你保證,調整你的DirectX代碼以使它渲染一個GDI表面而不是一個雙緩沖系統將比重新開發一整套新的繪制代碼簡單。記住過一段時間之后你的 GUI系統可能會改變,你不會想要經常在兩個不同的地方改寫代碼。


不要試圖模仿開發環境的外表和感覺。換句話說,不要費時間模仿開發環境的細節(例如,屬性頁和預覽窗口)。如果你的編輯器相對而言比較難看的話, 不要沮喪;的確,小組的效率是和他使用的工具的效能成直接正比關系的,但是同時,小組之外的人是不可能使用你的資源編輯器的,你也不會用它去開發一個完整 的GUI程序;你不過只是做幾個對話框而已。你不需要環境文本幫助(context sensitive help)。你不需要環境文本菜單(context menus),除非你覺得這會簡化一個特定的繁復的操作。如果你的資源編輯器不那么漂亮也無所謂,只要它能完成工作就行了。


強調數據完整而不是速度。資源編輯器是個數據整合者,而不是個高性能程序,沒有什么比你花了一個小時來設計的東西由于程序錯誤而丟失而更讓人惱火 的事兒了。當寫你的GUI的時候,保存數據是你的最高目標。花些時間來做自動存儲,釋放緩沖區(autosaves, undo buffers)等等,別把優化看得那么重要。
4.4、生成子類(subclass)
  那些熟悉Win32處理窗口的人們可能已經知道“subclass”這個術語的含義了。如果不知道的話,當你“subclass”一個窗口的時候,你就“衍生(derive)”一個新的窗口類型,然后把新的窗口類型嵌入到舊窗口要使用的地方。
  讓我做更詳細地解釋。比如我們需要一個超級列表框。我們已經有個普通的列表框類,但是因為某種原因它不適合;我們的游戲需要超級列表框。所以我們從普通的列表框類中衍生出一個超級列表框類。就是這樣。
  但是我們如何在我們的游戲對話框中放置這個超級列表框呢?由于超級列表框是為我們的程序特制的,我們不能為我們的資源編輯器增加函數來支持 它。但同時,我們怎樣通知GUI系統為這個特殊的實例(我們的游戲),讓所有的列表框都是超級列表框呢?這就是生成子類(subclass)要做的事情。 這不是個精確的技術定義,但是表達了足夠的信息。
  這里我要講述的方法稱作“載入過程生成子類(subclassing at load time)”。要理解它,讓我們從上一節所述的基本的載入代碼開始。我們有個載入函數,它第歸地完成創建,載入,增加窗口。這里我們用PDL表述如下:

  // read total number of children for this window
  // for each child...
    // read window ID from disk
    // new a gui_window derivative based on that ID
    // ...
  // next child

  要完成生成子類,我讓我的窗口載入函數“給程序一個創建這一類型窗口的機會“,像這樣:

  // read total number of children for this window
  // for each child...
    // read window ID from disk
    // give application a chance to create a window of this type
    // if the application didn't create a window,
      // then new a gui_window derivative based on the ID
    // else, use the application's created window
    // ...
  // next child

  我通過一個函數指針給程序這個機會。如果程序需要為一個窗口生成子類,就在函數指針里填上自己函數的地址。當窗口載入的過程中,調用這個函 數,傳入想要創建的窗口ID。如果程序想要根據ID為一個窗口生成子類,新建一個適當的對象并把新指針返回給窗口。如果程序不需要這個ID,則返回 NULL,窗口函數根據返回值創建恰當的默認對象。這種方法允許程序“預先過濾”引入的窗口ID信息,并為特定的窗口類型重載默認函數。太完美了(譯者: 這一段我翻譯得實在不能說是完美,相反是簡直不知所云,這里把原文貼出來,請您自己斟酌吧)。
  Specifically, I give the application this chance by way of a function pointer. If the application needs to subclass a window, it fills in the function pointer with the address of its own function. When the windows are loading, they call this application function, passing in the ID of the window they want to create. If the application wants to subclass a window from this ID, it news up the appropriate object and returns the new pointer back to the window. If the app doesn't want to do anything special for this ID, it returns NULL, and the window function senses this and news up the appropriate default object. This method allows the app to "pre-filter" the incoming window ID bytes, and to override the default behavior for certain window types. Perfect!

  用這種方法在創建自定控件的時候給了我很大的自由。我為我的資源編輯器增加代碼以使我能為每個存儲的窗口改變ID。然后,當我需要自定控間的時候,我只需用資源編輯器改變保存這個窗口ID的字節。在磁盤上保存的是ID和所有為自定控件的其他基類屬性。
  很快吧?還有其他的方法來做同樣的事,這是在模仿STL需要創建對象時使用的方法。STL使用特定的“allocator(分配符)”類,這有點兒像“類廠”(factories),它們按照客戶告訴他們的需要來創建類。你可以使用這種方法來創建窗口。
  這種方法的工作原理如下:創建一個類并把它叫做“gui_window_allocator”。寫一個虛擬函數,叫做 CreateWindowOfType,它接受一個給定的窗口ID并傳出一個新的指針給窗口。現在你就得到了一個簡單的分配符類,你的窗口載入代碼將使用 它來新建需要的窗口。
  現在,當你的程序需要為窗口重載“new”操作符,衍生一個新的、程序相關的gui_window_allocator類,并告訴你的窗口載入代碼來使用這個分配符,而不是默認的那個。這種方法就象提供一個函數指針,只用了一點兒C++。

4.5、加速GUI渲染
  還有一個能幫助你加速GUI渲染的小提示。
  關鍵的概念是,就象其它繪制函數的優化一樣,不要畫你不需要的東西。默認方式,GUI花很多時間繪制沒有變化的部分。然而,你能通過告訴 GUI繪制改變的窗口(windows that are dirty)作些優化。當窗口的外觀需要改變的時候,窗口設定它們的dirty標志,在繪制的時候清除它們的dirty標志。
  由于我們的GUI控件可能是透明的,當一個控件被標記為dirty,它的父窗口必須也被標記為dirty。這樣,當它被繪制的時候,背景沒有改變,因為父窗口也剛剛被重繪。

4.6、總結:相逢在XGDC
  呼,好長的4篇文章,然而我們還是遺漏了很多。
  我準備在即將來臨的Armageddon XGDC(eXtreme Game Developer's Conference)上講述GUI。我將盡力讓我的發言有用,現在你已經看了所有的文章,如果你有什么需要擴展的問題,或是我有什么遺漏,請給我寫信讓 我了解。祝各位做游戲愉快。


--------------------------------------------------------------------------------

  作者簡介:
  Mason McCuskey先生是Spin Studio(一個正在制作名為Quaternion的偉大游戲的制作小組)的領導者.他盼望著你的建議和評論,他的電子郵件是(mason@spin-studios.com)。

  譯者手記:
  終于結束了漫長的翻譯過程,這是Rick翻譯過的最長的也是難度最大的技術文章了。其中一定有不少的錯誤和遺漏,如果您發現了這些問題,請來信告訴我(rick_yinhua@sina.com)。謝謝您的支持。