实现C++GUI开发图形库

作者:文刀秋二
链接:https://www.zhihu.com/question/24462113/answer/40216675
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
 

我来补充一种比较新颖的GUI实现模式,叫做Immediate Mode GUI (IMGUI)。这种类型的更多的适用于显示区域实时刷新的程序里面,例如游戏和CAD等。IMGUI这种实现模式的优势在于他在实现和实用上都会比传统的Retained Mode GUI (RMGUI),例如Qt, MFC等,要简单不少。我自己两种模式都实现过,我认为IMGUI比较适合想学习自己写GUI的人上手。贴一个我自己实现的一个轻量级GUI库的效果图和程序,想玩玩的戳链接:EDXGui。可以看出IMGUI也可以实现较为复杂的控件,给自己的杂七杂八的程序用绰绰有余了。

 

<img src="https://pic1.zhimg.com/23cad02c136ad0d92ccb391770dc536c_b.jpg" data-rawheight="720" data-rawwidth="1280" class="origin_image zh-lightbox-thumb" width="1280" data-original="https://pic1.zhimg.com/23cad02c136ad0d92ccb391770dc536c_r.jpg">实现C++GUI开发图形库_第1张图片

接下来从使用和实现上分别谈谈传统RMGUI和IMGUI的区别,先来说说使用。用一个传统RMGUI库的时候,用户往往需要显式的初始化每一个控件对象。每个控件都是存在内存中的实体,并且每个控件都需要自己保存一部分数据(例如一个slider需要保存一个数值,Button要保存一个回调事件等),用户还需要在一个回调函数里将控件里的数据拷贝回程序本身中(MVC模式)。一般简单写成code大概就是这样:

 

GUIMgr* GUI;
const unsigned int ID_BUTTON = 1;
const unsigned int ID_SLIDER = 2;
float value = 0.5f;
void Init()
{
	GUI = new GUIMgr();
	GUI->Add(new Button(ID_BUTTON, "Click me", ...)); // 初始化每个控件对象
	Slider* slider = new Slider(new Slider(ID_SLIDER, "Slide me", 0.f, 1.f, ...));
	slider->SetValue(0.5f);
	GUI->Add(slider);
	GUI->SetCallbackFunc(&Callback);
}
void Callback(int widget_id)
{
	switch (widget_id)
	{
	case ID_BUTTON: OnClick(); break; 
	case ID_SLIDER: value = GUI->GetSlider(ID_SLIDER)->GetValue(); break;
	} // 回调函数中处理事件,App需要从控件中得到数据。
}
void main()
{
	Init();
	while (running)
		GUI->Render();
}

IMGUI模式在使用上会更简单粗暴一些。控件没有自己的对象,不保存任何状态,不用单独的去实现UI和程序间数据的交换,甚至都不需要单独为事件写回调函数。每个控件就是一个函数,直接在程序的Draw()函数里要哪个控件就调用哪个函数就好了。同样上面例子的控件写成code大概是这样:

GUIState* pState;
void RenderGUI(GUIState* pState, float& value)
{
	if (Button(pState, "Click me", …)) // 每个控件就是一个函数
		DoButtonAction();
	Slider(pState, "Slide me", value, 0.f, 1.f, …); // 控件函数直接修改App数据,不需要explicit的去传递
}
void WindowProc()
{
	Update(pState, ...); // 获取基本鼠标键盘等IO消息
}
void main()
{
	pState = new GUIState;
	float myValue = 0.5f;
	while (running)
		RenderGUI(pState, myValue);
}

拿我自己的GUI库举个例子,弄出图片里三个窗口,一堆控件需要的所有代码就是这些,不用单独的写callback函数拷贝数据,不用单独初始化每个控件,实在爽快。

<img src="https://pic1.zhimg.com/da366978656beefb1ea3489ae938cd8c_b.jpg" data-rawheight="1287" data-rawwidth="417" class="content_image" width="417">实现C++GUI开发图形库_第2张图片

再说下实现上的差别。RMGUI模式里每一个控件都是一个对象,自然你得给每个控件弄一个类,不同的控件有不同的成员变量保存不同的数据。每一个控件都要各自实现自己的交互的函数,绘制的函数,还有返回数据的函数。然后用一个GUIManager统一管理控件,分配事件,绘制控件。代码大概长这样:

class GUIMgr
{
private:
	Array Controls;
	NotifyEvent CallbackEvent;
	// GUI Manager统一管理所有控件
public:
	void Init(...);
	void AddControl(Control* cont) { Controls.Add(cont); }
	void SetCallback(NotifyEvent event) { CallbackEvent = event; }
	void Render() { foreach control in Controls control->Render(); }
	void HandleMsg(Message msg)
	{
		// 处理消息后调用Callback传给App
		Control* cont = SelectControl(msg);
		cont->HandleMsg(msg);
		CallbackEvent->Invoke(cont);
	}
};
class Slider : public Control
{
private:
	float Min; // 每个控件有自己的状态
	float Max;
	float Val;

public:
	// 每个控件都要实现若干接口
	void Init(...);
	void Render() const;
	float GetValue() const { return Val; }
	bool HandleMsg(Message msg);
};

RMGUI的逻辑实在复杂,我这也只是随手写了个大概的样子就快晕了,相比之下IMGUI则每一个控件都是一个大函数,既处理交互,又做渲染。所以IMGUI的控件本身不储存任何数据,数据全部都放在App里,直接在函数里对数据修改和用数据渲染。代码大概长这样。

void Slider(const char* str, float* pVal, float min, float max)
{
	int Id = GUIState.Id++; // Id隐式的cache在state里面
	Message msg = GUIState.Msg; // 从State中拿到IO消息

	if (msg.LButtonDown && InRect(GUIState)) // 当前控件的位置信息也可以存在Cache里面
		GUIState.ActiveId = Id;
	if (Id == GUIState.ActiveId && msg.MouseMove)
	{
		// 处理消息。。直接修改App的pVal
		pVal = LinStep(msg.MousePos.x, Base, End);
	}

	// 渲染
	DrawString(str);
	DrawBarAndButton(...);
}

因此IMGUI的代码会相对紧凑许多,所有功能都紧凑的放到一块儿了,所以往往实现时候需要的代码量少一些,我自己图片里的那个库有一个功能还算全的文本输入框,代码也不过几百行。而且因为不用专门处理数据的交互和回调函数,实现起来也会更简单一点。

 

口水了那么多,IMGUI自然也不是十全十美的。例如我个人就觉得正因为它每个控件没有自己的状态,要做一些Animation时候就比较蛋疼,animation 至少需要每个控件有一个时间变量吧。当然硬要实现也是可以的,你可以自己加额外的数据结构去保存,只不过那样写起来就完全失去了优雅而且还不如写RMGUI简单了。其他问题例如所有控件的交互逻辑每一帧都要被执行一次也许影响性能(试想一个很差的实现每帧都要排版一个几百万行的字符串60次)。有些人也不喜欢这种把不同功能的代码累在一起的风格。再有就是不太适合那些显示区域不实时绘制的程序。。

 

但是这些问题对于自己写的小程序来说实践中影响都不大。著名的游戏引擎Unity的GUI就是用IMGUI 模式写的。其他的例子例如这个程序的GUI也是Immediate Mode的,可见IMGUI也是可以实现非常复杂的控件。其他例子例如这个程序Tinkercad | Create 3D digital designs with online CAD

<img src="https://pic1.zhimg.com/f6c36a5b7846d28536e252db311d5ac0_b.jpg" data-rawheight="400" data-rawwidth="534" class="origin_image zh-lightbox-thumb" width="534" data-original="https://pic1.zhimg.com/f6c36a5b7846d28536e252db311d5ac0_r.jpg">实现C++GUI开发图形库_第3张图片

 

最后给几个IMGUI相关的链接:

www.iki.fi/sol 不错的基础教程

http://www.cse.chalmers.se/edu/year/2011/course/TDA361/Advanced%20Computer%20Graphics/IMGUI.pdf 介绍的pdf

你可能感兴趣的:(实现C++GUI开发图形库)