原文
SonicUI
,你从未见过的方便GUI
引擎-源码
介绍
SonicUI
是基于原生GDIAPI
的GUI
引擎.它提供了几个简单的UI
组件来实现
高效的UI
效果,如自绘按钮,不规则窗口,动画
,窗口
中的网径
和图像
操作方法.
主要目的
是用最少的代码
来达到最佳效果
.
背景
周知,UI
开发一般重复用无趣
.因此,设计此引擎
时遵守了两个原则
:易于使用和高效
.看看下面引擎的用法
,和一些有趣的要点
.
使用代码
首先,让我介绍类工厂和组件管
:ISonicUI
.用此接口来创建和析构对象
,并用作某些全局函数
.
1
.显示和旋转图像
图像操作接口
:ISonicImage
,用来加载,保存,旋转,拉伸,灰度
或HSL
调整等.感谢CxImage
的作者,我使用此库
来避免编码或解码多种图像格式
.
但是,在由CxImage加载图像
并按标准dib
格式转换后,我自己处理它们.ISonicImage
的用法
很简单:
ISonicImage * pImg = GetSonicUI()->CreateImage();
pImg->Load("C:\\demo.png");
pImg->Rotate(90);
pImg->Draw(hdc);
2
.制作一个网径
使用某些控件
输出彩色串
,或向窗口
添加网径
可能很无聊
.使用原生GDIAPI
,你必须不断地选择不同字体
或其他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
并设置其格式
,以避免重复初化
,及在BeginPaint()
和EndPaint()
间放置pStr->TextOut()
方法.
是的,在没有在窗口中放置控件
,或不关心无聊的分发消息
时,仅需三行
即可构成
一个网径
,这似乎是不可能的吗?嗯,这只是子类
的把戏.
这样,你可让你的代码
与超文本
代码一样简单.你唯一要参加
的是ISonicString
的关键字.你可在ISonicUI.h
接口文件中找到特定的说明
.
3
.制作动画自绘按钮
对UI
人,也很熟悉自定义按钮
.使用ISonicString
,可轻松创建漂亮的按钮
,只与创建网径
稍有不同.
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
.如果你取得
一个包含三个状态
的源图像
,则也没有关系
.
只需要按ISonicImage
的相同对象ID
指定"p,ph,pc"
,ISonicImage
会完成所有操作.内部就搞了源矩
剪切.
"animation=40"
表示这是一个阴影按钮
,即,在切换状态
时显示动画
.40
是阴影速度
,越高,速度越快
.Delegate()
方法给按回调
转发点击
事件的过程,然后,如果你点击按钮
,则调用该过程
.
4
.制作不规则窗口
ISonicWndEffect
组件用来造不规则窗口
,或给窗口
加动画,如平滑移动,平滑旋转或平滑拉伸
等.制作不规则窗口的方法
有两个:使用窗口区域
或分层窗口
.
首先是窗口区域
方式:
...
// ISonicImage * pImg
SetWindowRgn(hWnd, pImg->CreateRgn());
第二个是使用分层窗口
:
...
ISonicWndEffect * pEffect = GetSonicUI()->CeateWndEffect();
// 用α每像素附加模式
pEffect->Attach(hWnd, TRUE);
// ISonicImage * pImg
pEffect->SetShapeByImage(pImg);
5
.其他组件
还有许多其他组件
,如ISonicTextScrollBar
和ISonicAnimation
,你可用它们实现
许多熟悉的UI
效果,如滚动文本,平滑移动图片,旋转或平滑拉伸
并很爽.
用法非常简单
,可在ISonicUI.h
接口文件中查找它.在此,这里说下兴趣点
.
兴趣点
这里展示一些包含ASM
和API
勾挂技术的技巧
.
1
.闭包
当然,希望找到简单方法
来给不同过程
转发自绘按钮
,以可普遍使用组件
.但是函数声明
中有问题.VC
禁止你按普通方式
按参数,传递类的成员函数
.
你必须使用与类相关
的成员函数指针
,且显然违反"通用"
原则.因此,我使用易失
参数来避免该限制
.
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;
}
而且也无法正常回调
.不用担心,只需一点ASM
代码即可完成工作
.
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 ecxpush [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
消息,不会显示
你的所有子控件
,且通用的GDIAPI
可能无法正常工作
,而SetLayeredWindowAttributes
使用重定向机制
来使一切正常运行
.
似乎UpdateLayeredWindow
只是一个麻烦制造者
,但是如果你要创建
一个每像素α
窗口并按背景使用PNG
来实现某些阴影效果
,则只能选择UpdateLayeredWindow
.
因为ISonicWndEffect
只是,在现有窗柄
上附加的"附件"
,我该如何要求引擎用户
重写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;
}
这样,当我想使用
(内部由UpdateLayeredWindow
实现的)每像素α
模式重画
由ISonicWndEffect
附加的窗口
时,我只是向窗口
发送了假WM_PAINT
消息,并按WM_PAINT
的字参
使用memdc
,无需更改渲染代码
就正确渲染
了.
实际上,使用此技巧
带来为一些礼物
.使用此引擎
时,即使隐藏了窗口
,也可在指定memdc
上绘画所有窗口
.
结论
还有许多其他技巧和技术
,如按整
转换浮
运算,更新脏矩形
机制,SSE2
指令等,以优化引擎效率
.
历史
更改了函数勾挂代码
,以避免泄漏内存警告
.修改了CSonicString::TextOut
一些代码,使其可与内存dc
一起使用,而无需指定窗柄
.给ISonicImage
添加了高斯模糊
甚至模糊功能.
修复了ISonicImage
中的可能导致崩溃
的服务器错误
.给ISonicWndEffect
添加DirectTransfrom
方法.给ISonicTextScrollBar
添加了一些功能.
添加了ISonicSkin
组件.可仅用三行代码
来装饰窗口和对话框
.很酷,很方便
!添加了统一
支持,添加了静态库
输出支持.
几种类型从MFC
,更改为ATL
支持,以减轻引擎重量
.优化
了一些内核工具
,如脏矩形更新机制
.
为ISonicUI
和ISonicString
添加了接口,ISonicString
中修改了"p"
关键字的格式
.
这是一个制作带四种状态
的平铺图像的自绘按钮
的示例
:
ISonicString::Format("/a, p4=%d/", pImg->GetObjectId());
已丢弃
原来的"ph"
和"pc"
关键字.见ISonicUI.h
以获取更多细节
.