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






4.92/5 (75投票s)
一个方便且功能强大的 GUI 引擎,包含大量技巧

引言
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. 其他组件
还有许多其他组件,例如 ISonicTextScrollBar
和 ISonicAnimation
,使用它们可以实现许多熟悉的 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 用于显示分层窗口:SetLayeredWindowAttributes
和 UpdateLayeredWindow
。尽管 MSDN 说 SetLayeredWindowAttributes
内部使用了 UpdateLayeredWindow
,但对于应用程序开发者来说,这两个函数之间存在致命的差异。为了进一步讨论,我可能会另外开一篇文章。但在这里我只能说主要区别是使用 UpdateLayeredWindow
时,WM_PAINT
消息将被放弃,您所有的子控件都将无法显示,而通用 GDI API 可能无法正常工作,而 SetLayeredWindowAttributes
使用重定向机制来确保一切正常工作。听起来 UpdateLayeredWindow
只是个麻烦制造者,但是如果您想创建一个每像素 alpha 的窗口并使用 PNG 作为背景来实现一些阴影效果,UpdateLayeredWindow
将是唯一的选择。
由于 ISonicWndEffect
只是一个“附件”,它附加到一个现有的 hwnd
上,我怎么能要求引擎用户重写 BeginPaint
和 EndPaint
之间的所有渲染代码呢?所以我使用了一个 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_PAINT
的 wParam
,所有内容都将正确渲染,而无需更改渲染代码。
事实上,使用这个技巧会给我们带来一点好处。当这个引擎在您的进程中运行时,您所有的窗口都可以绘制到一个指定的 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 支持,以使引擎更轻量
- 优化了部分内核实现,例如脏矩形更新机制
- 向
ISonicUI
、ISonicString
添加了接口 - 修改了
ISonicString
中关键字“p”的格式。例如,要使用 4 状态平铺图像创建自绘按钮:ISonicString::Format("/a, p4=%d/", pImg->GetObjectId());
。丢弃了原始的“ph”和“pc”关键字。有关更多详细信息,请参阅 ISonicUI.h。