Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

SonicUI - A Convenient GUI Engine You've Never Seen

0.00/5 (No votes)
26 May 2010 1  
A convenient and high-powered GUI engine with plenty of tricks
game.jpg

task.jpg

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:

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

Second using layered window:

...
ISonicWndEffect * pEffect = GetSonicUI()->CeateWndEffect();
// use alpha-per-pixel attaching mode
pEffect->Attach(hWnd, TRUE);
// ISonicImage * pImg
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
    • First post
  • 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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here