65.9K
CodeProject 正在变化。 阅读更多。
Home

SonicUI - 您从未见过的便捷 GUI 引擎

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (75投票s)

2008年12月13日

CPOL

7分钟阅读

viewsIcon

6022418

downloadIcon

34812

一个方便且功能强大的 GUI 引擎,包含大量技巧

game.jpg

task.jpg

引言

SonicUI 是一个基于原生 GDI API 的 GUI 引擎。它提供了几个简单的 UI 组件来完成高效的 UI 效果,例如自绘按钮、不规则窗口、动画、窗口内的 URL 以及图像操作方法。主要目的是用最少的代码实现最佳效果。

背景

最近,通用应用程序 UI 开发越来越像游戏开发。许多商业软件中随处可见自绘组件,例如中国最流行的即时通讯软件 QQ2009。我曾经是一家公司的休闲游戏开发者,所以我也想将一些游戏开发机制引入到我目前的工作中。带着这个想法,我编写了这个 GUI 引擎来帮助。众所周知,UI 开发通常是一项重复且无聊的工作。所以我设计了这个引擎,遵循两个原则:易于使用和高效。让我们看看下面的引擎用法,您会发现一些有趣的地方。

Using the Code

首先,让我介绍类工厂和组件管理器:ISonicUI。此接口用于创建和销毁对象,并充当一些全局函数。

1. 显示和旋转图像

图像操作接口:ISonicImage,用于加载、保存、旋转、拉伸、灰度化或进行 HSL 调整等。感谢 CxImage 的作者,我使用这个库来避免编码或解码各种复杂的图像格式。但是,在图像由 CxImage 加载并转换为标准 dib 格式后,我以自己的方式处理它们。ISonicImage 的用法很简单。

ISonicImage * pImg = GetSonicUI()->CreateImage();
pImg->Load("C:\\demo.png");
pImg->Rotate(90);
pImg->Draw(hdc);

2. 创建 URL

输出彩色的 string 或使用某些控件向窗口添加 URL 可能会很枯燥。使用原生 GDI API,您必须反复地将不同的字体或其他 GDI 对象选入和选出 dc。但有了 ISonicString,您只需三四行即可完成任务。

ISonicString * pStr = GetSonicUI()->CreateString();
pStr->Format("/a='http://hi.csdn.net/zskof', c=%x/Hello I'm a clik", RGB(0, 0, 255));
.
.
.
pStr->TextOut(hdc, 10, 10);

注意:不要在 WM_PAINT 过程中创建和格式化 ISonicString,以避免重复初始化,并将 pStr->TextOut() 方法放在 BeginPaint()EndPaint() 之间。

是的,三行即可创建一个 URL,而无需在窗口中放置任何控件,也无需关注无聊的消息分派,这似乎不可能?嗯,这只是子类技巧。通过这种方式,您可以使代码像 HTML 代码一样简单。您唯一需要关注的是 ISonicString 的关键字。您可以在接口文件 ISonicUI.h 中找到具体的解释。

3. 创建动画自绘按钮

自绘按钮对我们 UI 开发者来说也很熟悉。使用 ISonicString,我们可以轻松创建一个漂亮的按钮,与创建 URL 几乎一样简单。

void WINAPI OnMove(ISonicString * pStr, LPVOID)
{
	...
}

ISonicImage * pImgNormal = GetSonicUI()->CreateImage();
pImgNormal->Load(BMP_NORMAL);
pImgNormal->SetColorKey(RGB(255, 0, 255));

ISonicImage * pImgHover = GetSonicUI()->CreateImage();
pImgHover->Load(BMP_HOVER);
pImgHover->SetColorKey(RGB(255, 0, 255));

ISonicImage * pImgClick = GetSonicUI()->CreateImage();
pImgClick->Load(BMP_CLICK);
pImgClick->SetColorKey(RGB(255, 0, 255));

ISonicString * pAniButton = GetSonicUI()->CreateString();
pAniButton->Format("/a, p=%d, ph=%d, pc=%d, animation=40/",
    pImgNormal->GetObjectId(), pImgHover->GetObjectId(), pImgClick->GetObjectId());
pAniButton->Delegate(DELEGATE_EVENT_CLICK, NULL, NULL, OnMove);
pAniButton->TextOut(hdc, 10, 10);

p, ph, pc” 关键字代表按钮的三个状态(正常、悬停、单击)。每个关键字都指定一个 ISonicImage 作为其显示项。如果您有一个包含三个状态的源图像,也没关系。您只需将“p, ph, pc”指定到同一个 ISonicImage 对象 ID,一切都会完成。我会在内部为您进行源矩形剪裁。“animation=40”表示这是一个渐变按钮,换句话说,动画将在状态切换期间显示。40 是渐变速度,数值越大,速度越快。Delegate() 方法将一个过程作为回调委托给单击事件,如果您单击按钮,该过程将被调用。稍后我们将详细讨论委托技巧。

4. 创建不规则窗口

ISonicWndEffect 组件用于创建不规则窗口,或让窗口执行某些动画,例如平滑移动、旋转或平滑拉伸等。有两种方法可以创建不规则窗口:使用窗口 Rgn 或分层窗口。首先是窗口 rgn 方法。

...
// ISonicImage * pImg
SetWindowRgn(hWnd, pImg->CreateRgn());

其次是使用分层窗口。

...
ISonicWndEffect * pEffect = GetSonicUI()->CeateWndEffect();
// use alpha-per-pixel attaching mode
pEffect->Attach(hWnd, TRUE);
// ISonicImage * pImg
pEffect->SetShapeByImage(pImg);

5. 其他组件

还有许多其他组件,例如 ISonicTextScrollBarISonicAnimation,使用它们可以实现许多熟悉的 UI 效果,例如滚动文本、平滑移动图片、以良好的视觉效果平滑旋转或拉伸图片。用法非常简单,您可以在接口文件 ISonicUI.h 中查找。我将在这里节省一些篇幅,留给下面的更令人兴奋的部分。

关注点

本章我将展示我的项目中的几个技巧。这些技巧包含汇编和 API 挂钩技术。

1. 委托

当然,我们希望找到一种简单的方法将不同的过程委托给自绘按钮,以便使组件能够通用。但是函数声明存在一个问题。VC++不允许您以常规方式将类的成员函数作为参数传递。您必须使用成员函数指针,它与类相关,显然违背了“通用”原则。因此,我使用 volatile 参数来避免限制。

void ISonicBase::Delegate(UINT message, LPVOID pReserve, LPVOID pClass, ...)
{
	if(IsValid() == FALSE)
	{
		return;
	}
	ISonicBaseData * pData = dynamic_cast(this);
	if(pData == NULL)
	{
		return;
	}
	DELEGATE_PARAM pm = {0};
	pm.pClass = pClass;
	pm.pReserve = pReserve;
	va_list argPtr;
	va_start(argPtr, pClass);
	pm.pFunc = va_arg(argPtr, LPVOID);
	va_end(argPtr);
	pData->m_mapDelegate[message] = pm;
}

而且我们也无法以常规方式进行回调。别担心,只需要一点汇编代码即可完成。

void ISonicBaseData::OnDelegate(UINT message)
{
	MSG_TO_DELEGATE_PARAM::iterator it = m_mapDelegate.find(message);
	if(it == m_mapDelegate.end())
	{
		return;
	}
	DELEGATE_PARAM &pm = it->second;
	if(pm.pFunc == NULL || IsBadCodePtr((FARPROC)pm.pFunc))
	{
		return;
	}
	ISonicBase * pBase = dynamic_cast(this);
	if(pBase == NULL)
	{
		return;
	}
	LPVOID pReserve = pm.pReserve;
	LPVOID pClass = pm.pClass;
	LPVOID pFunc = pm.pFunc;
	__asm
	{
		push ecx
		push [pReserve]
		push [pBase]
		mov ecx, [pClass]
		call [pFunc]
		pop ecx
	}
}

在某些方面,C++ 语法检查的安全性被我们破坏了,因此您必须确保回调函数的声明严格遵守规则。

void WINPAI Func(ISonicBase *, LPVOID)

否则,您将遇到堆栈崩溃或其他致命错误。

2. 分层窗口

分层窗口广泛用于实现透明窗口或不规则窗口。有两个 API 用于显示分层窗口:SetLayeredWindowAttributesUpdateLayeredWindow。尽管 MSDN 说 SetLayeredWindowAttributes 内部使用了 UpdateLayeredWindow,但对于应用程序开发者来说,这两个函数之间存在致命的差异。为了进一步讨论,我可能会另外开一篇文章。但在这里我只能说主要区别是使用 UpdateLayeredWindow 时,WM_PAINT 消息将被放弃,您所有的子控件都将无法显示,而通用 GDI API 可能无法正常工作,而 SetLayeredWindowAttributes 使用重定向机制来确保一切正常工作。听起来 UpdateLayeredWindow 只是个麻烦制造者,但是如果您想创建一个每像素 alpha 的窗口并使用 PNG 作为背景来实现一些阴影效果,UpdateLayeredWindow 将是唯一的选择。

由于 ISonicWndEffect 只是一个“附件”,它附加到一个现有的 hwnd 上,我怎么能要求引擎用户重写 BeginPaintEndPaint 之间的所有渲染代码呢?所以我使用了一个 API 挂钩技巧。

	// ...
	HMODULE hMod = GetModuleHandle("User32.dll");
	if(hMod == NULL)
	{
		return FALSE;
	}
	m_pOldBeginPaint = ReplaceFuncAndCopy(GetProcAddress
				(hMod, "BeginPaint"), MyBeginPaint);
	m_pOldEndPaint = ReplaceFuncAndCopy(GetProcAddress
				(hMod, "EndPaint"), MyEndPaint);

HDC CSonicUI::MyBeginPaint( HWND hwnd, LPPAINTSTRUCT lpPaint )
{
	HDC hdc;
	if(m_hPaintDC)
	{
		memset(lpPaint, 0, sizeof(PAINTSTRUCT));
		lpPaint->hdc = m_hPaintDC;
		GetClientRect(hwnd, &lpPaint->rcPaint);
		hdc = m_hPaintDC;
		g_UI.m_rtUpdate = lpPaint->rcPaint;
	}
	else
	{
		GetUpdateRect(hwnd, g_UI.m_rtUpdate, FALSE);
		__asm
		{
			push [ebp + 0ch]
			push [ebp + 8h]
			call [m_pOldBeginPaint]
			mov [hdc], eax
		}
	}
	g_UI.m_bPainting = TRUE;
	return hdc;
}

BOOL CSonicUI::MyEndPaint( HWND hWnd, CONST PAINTSTRUCT *lpPaint )
{
	BOOL bRet = TRUE;
	// ...
	if(m_hPaintDC)
	{
		m_hPaintDC = NULL;
		return TRUE;
	}
	else
	{
		__asm
		{
			push [ebp + 0ch]
			push [ebp + 8h]
			call [m_pOldEndPaint]
			mov [bRet], eax
		}
	}
	GetClientRect(hWnd, g_UI.m_rtUpdate);
	g_UI.m_bPainting = FALSE;
	return bRet;
}

通过这种方式,当我需要使用每像素 alpha 模式(内部使用 UpdateLayeredWindow 实现)重绘由 ISonicWndEffect 附加的窗口时,我只需要向窗口发送一个假的 WM_PAINT 消息,并将一个 memdc 作为 WM_PAINTwParam,所有内容都将正确渲染,而无需更改渲染代码。

事实上,使用这个技巧会给我们带来一点好处。当这个引擎在您的进程中运行时,您所有的窗口都可以绘制到一个指定的 memdc,即使它是隐藏的。

结论

还有许多其他技巧和技术,例如将 float 操作转换为 integer、脏矩形更新机制、SSE2 指令等,以优化引擎的效率。我将把这些部分留给读者通过我的代码来探索。希望您喜欢我的引擎,如果您有任何好的想法或建议,请联系我。

历史

  • 2008 年 12 月 13 日
    • 第一篇帖子
  • 2009 年 1 月 13 日
    • 更改了函数挂钩代码以避免内存泄漏警告
    • 修改了 CSonicString::TextOut 中的一些代码,使其能够在不指定 hwnd 的情况下与内存 dc 一起使用
    • ISonicImage 添加了高斯模糊和均值模糊功能
  • 2009 年 3 月 15 日
    • 修复了 ISonicImage 中可能导致崩溃的服务器 bug
    • ISonicWndEffect 添加了 DirectTransfrom 方法
    • ISonicTextScrollBar 添加了一些功能
  • 2010 年 5 月 25 日
    • 添加了 ISonicSkin 组件。只需三行代码即可美化您的窗口和对话框。它很酷而且方便!
    • 添加了Unicode支持。
    • 添加了静态库输出
    • 将一些类型从 MFC 支持更改为 ATL 支持,以使引擎更轻量
    • 优化了部分内核实现,例如脏矩形更新机制
    • ISonicUIISonicString 添加了接口
    • 修改了 ISonicString 中关键字“p”的格式。例如,要使用 4 状态平铺图像创建自绘按钮:ISonicString::Format("/a, p4=%d/", pImg->GetObjectId());。丢弃了原始的“ph”和“pc”关键字。有关更多详细信息,请参阅 ISonicUI.h
© . All rights reserved.