Introduction
SonicUI
is a GUI Engine based on nake GDI APIs. It offers several simple UI components to accomplish high efficiency UI effects, such as self-draw buttons, irregular windows, animation, URL in windows and image operation methods. The main purpose is to use least code to achieve best effects.
Background
Recently generic application UI development is becoming more like game development. Self-draw components are used here and there in many merchant software, such as the most popular IM software in China - QQ2009. I have been a leisure game developer in a company, so I would also like to introduce some game developing mechanisms into my current work. With this thought, I wrote this GUI engine to help. As everyone knows, UI development is often a repeated and disinteresting job. So I designed this engine with two principles: simple to use and high efficiency. Let's take a look into the usage of the engine below, you will find some interesting points.
Using the Code
First of all, let me introduce the class factory and components manager: ISonicUI
. This interface is used for creating and destroying objects and act as some global function.
1. Showing and Rotating an Image
The image operation interface: ISonicImage
, which is used to load, save, rotate, stretch, gray or do HSL adjustment, etc. Thanks to the author of CxImage
, I use this lib to avoid encoding or decoding multifarious formats of image. But after the images are loaded by CxImage
and converted to standard dib format, I deal with them in my own way. The usage of ISonicImage
is simple:
ISonicImage * pImg = GetSonicUI()->CreateImage();
pImg->Load("C:\\demo.png");
pImg->Rotate(90);
pImg->Draw(hdc);
2. Making a URL
It may be boring to output a colorful string
or add a URL to the window using some controls. With the nake GDI APIs, you have to select a different font or other GDI objects into and out from dc again and again. But with ISonicString
, you will accomplish the job in just three or four lines.
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);
Notice: Don't create and format ISonicString
in WM_PAINT
procedure to avoid repeated initialization and put the pStr->TextOut()
method between BeginPaint()
and EndPaint()
.
Yes, three lines to make a URL, without putting any controls in the window or paying attention to boring message dispatch, seems impossible? Hmm, just subclass tricks. In this way, you can make your code just as simple as HTML code. The only thing you will attend is the keywords of ISonicString
. You can find a particular explanation in the interface file - ISonicUI.h.
3. Making an Animation Self-draw Button
Self-draw button is also familiar to us UI developers. With ISonicString
, we can easily make a beautiful button with just a little difference from making a 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);
The "p
, ph
, pc
" keywords stand for three statuses (normal, hover, click) of a button. Every keyword appoints a ISonicImage
as its displaying item. If you get a source image, which tiles three status, then it doesn't matter either. You just need to appoint "p
, ph
, pc
" to the same object id of an ISonicImage
and everything will be done. I do the source rect clip internally for you. The "animation=40
" figures that this is a shading button, in other words, animation will be displayed during status switch. 40
is the shading speed, the higher, the faster. The Delegate()
method delegates a procedure to the click event as a callback, and then the procedure will be called if you click the button. We will talk about more details on Delegation trick later.
4. Making an Irregular Window
ISonicWndEffect
component is used for making irregular windows, or to make windows do some animation, such as moving smoothly, rotating or stretching smoothly, etc. There are two methods to make irregular windows: using window Rgn
or layered window. First the window rgn
way:
...
SetWindowRgn(hWnd, pImg->CreateRgn());
Second using layered window:
...
ISonicWndEffect * pEffect = GetSonicUI()->CeateWndEffect();
pEffect->Attach(hWnd, TRUE);
pEffect->SetShapeByImage(pImg);
5. Other Components
There are many other components, like ISonicTextScrollBar
and ISonicAnimation
, with which you can implement lots of familiar UI effects, such as scroll text, moving a picture smoothly, rotating or stretching it smoothly with a good visual sense. The usage is rather easy and you can look it up in the interface file ISonicUI.h. Here I will save my words for the more interesting part below.
Points of Interest
I will show several tricks in my project in this chapter. These tricks contain ASM and API hook techniques.
1. Delegation
Of course we want to find a simple way to delegate different procedures to self-draw buttons so as to make the components capable of being universally used. But there is a problem in function declaration. VC++ doesn't permit you to transfer a member function of a class as a parameter in the normal way. You have to use member function pointer, which is class relevant and obviously against the "universal" principle. So I use the volatile parameter to avoid the restriction.
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;
}
And we cannot make a callback in the normal way either. Don't worry, just a little ASM code will do the job.
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
}
}
In some respects, the security of C++ syntax check is wrecked by us, so you must ensure the declaration of the callback function strictly obeys the rule:
void WINPAI Func(ISonicBase *, LPVOID)
Otherwise you will get a stack crash or some fatal errors.
2. Layered Window
Layered Window is widely used for implementing transparent windows or irregular windows. There are two APIs used for displaying a layered window, SetLayeredWindowAttributes
and UpdateLayeredWindow
. But there's a mortal difference between these two functions to application developers although SetLayeredWindowAttributes
uses UpdateLayeredWindow
internally as MSDN said. For further discussion, I may start another article on it. But here I can just say the main difference is when UpdateLayeredWindow
is used, WM_PAINT
message is abandoned, all your child controls will not show themselves, and generic GDI APIs may work incorrectly while SetLayeredWindowAttributes
uses a redirected mechanism to keep everything working well. Sounds like the UpdateLayeredWindow
is just a trouble maker, but if you want to make an alpha-per-pixel window and use a PNG as the background to implement some shadow effects, UpdateLayeredWindow
will be the only choice.
Since ISonicWndEffect
is just an "attachment" which attaches to an existing hwnd
, how can I demand the engine user to rewrite all his rendering code between BeginPaint
and EndPaint
? So I use an API hook trick.
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;
}
By this way, when I want to redraw the window attached by ISonicWndEffect
using alpha-per-pixel mode (internally implemented with UpdateLayeredWindow
), I just send the window a fake WM_PAINT
message and use a memdc
as the wParam
of WM_PAINT
, everything will be correctly rendered, without any change to the rendering code.
In fact, using this trick would bring us a little present. When this engine is in your process, all your windows can be drawn to a specified memdc
, even when it's hidden.
Conclusion
There are still many other tricks and techniques, such as convert float
operation to integer
, dirty rect update mechanism, SSE2 instructions, etc. to optimize the efficiency of the engine. I will leave these parts to readers with my code. I hope you enjoy my engine and contact me if you have any good ideas or suggestions.
History
- 13th December, 2008
- 13th January, 2009
- Changed the function hook code to avoid memory leak warnings
- Modified some code within
CSonicString::TextOut
to make it capable of being used with memory dc without specifying a hwnd
- Added gauss blur and even blur feature to
ISonicImage
- 15th March, 2009
- Fixed server bugs in
ISonicImage
which may cause crash
- Added
DirectTransfrom
method to ISonicWndEffect
Added some features to ISonicTextScrollBar
- 25th May, 2010
- Added
ISonicSkin
component. You can prettify your windows and dialogs with only three lines of code. It's cool and handy!
- Added Unicode support
- Added static library output
- Changed several types from MFC support to ATL support to make the engine lighter
- Optimised some kernel implements, such as dirty rect update mechanism
- Added interfaces to
ISonicUI
, ISonicString
- Modified format of keyword "p" in
ISonicString
. Here is an example to make a self-draw button with a 4-state tiled image: ISonicString::Format("/a, p4=%d/", pImg->GetObjectId());
. The original "ph" and "pc" keywords were discarded. Refer to ISonicUI.h to get more details.