Background
Warning!!
In order to understand this article well and its source, you should have some knowledge of WinAPI functions, constants, and structs. If you don't know what I'm talking about it will be hard to just figure it out. Look at the "Points of Interest" section to find out where you can start your search for knowledge.
In the beginning...
Lately, I've been working on a Felix the Cat Desktop Companion. I became interested in cartoon-styled balloons tooltips when I wanted Felix to "talk" to the user. Since C# doesn't have a Balloon control class and I really wanted to make Felix "talk", I started to look at how to make it happen. I knew there were at least two paths to follow:
- The first was to implement a Balloon class using GDI+;
- The second was to see whatever Windows was using to display its balloons and see if I could use it too.
Both paths were dark for me:
- I knew some GDI+ but not well enough, I also knew that regions and graphs demand a lot of processor and there was no small source code in the search results, and top of all, it looked hard to master. It's not that I was afraid or something, I just didn't feel like getting into GDI+ just to get a darn balloon. For the record, GDI+ is a great tool, believe me, once you get the hold of it you can get some pretty cool stuff done.
- I also knew what the WinAPI is, what could be done and how powerful it is. So there was no doubt in my mind that there was an easier way to get Balloons through it.
With these facts on the table, I took my chances with the WinAPI.
And there was light...
The first place I looked in to see if anyone had implemented some code was in CodeProject. I found two articles, one was implemented with GDI+, the other one was made using WinAPI.
I downloaded the WinAPI demo and its code thinking my problem was solved.
I guess not...
As any curious person would normally do, I executed the demo first, I started to click the buttons here and there, I also read what the forms said, hoping to figure out what the program did. My clicks were in vain; out of many buttons, out of three forms, only one of them displayed a balloon tooltip and left it hanging in the screen until the application ended, letting the user know that there was a bad resource management. After that I decided to look at the code; there were just a few comments, several and useless classes that did not work as they should or did not work at all, and most important, I didn't understand what the author had done nor why or how he had done it, in other words, there was no clear idea on how or where to get started. I was not happy.
I decided to look at the GDI+ solution, so I downloaded and executed its demo project. This time I was surprised. Someone had done a cool multifunctional balloon tooltip, in fact too cool and therefore too complex. It had a lot of extra stuff I just didn't need, I didn't feel like going through the code to modify it, and I was not too keen on GDI+. The only solution for me at that point was to do something better using WinAPI.
When someone does something better, there's always a starting point, mine was the same project that had disappointed me, the Ballons Tips Galore article. This doesn't mean that I stole his code, not at all, you can see by yourself that the implementation differs. For example, to create the balloon window, he uses the NativeWindow
class and I use the CreateWindowEx
WinAPI function. So no, this is not even a remake, I just used the code to get an idea on how a balloon tooltip window was created and where I could start my search.
The thing is every time I want to do something new with WinAPI, I can't find any real help in MSDN; yes, the constant's names are there, but not their real values; yes, the API is explained, but there's no useful code that shows you how to put it all together. So you see, the only way that has worked for me is looking at someone else's code and learning from it. This is why my source is available and well documented, so that anyone can learn from it, understand what, why and how something is done.
Where I started...
MSDN has a search engine. I typed tooltip without a filter, more than 500 results came out; I typed Tooltip controls, 500 results came out again, but the first one was an exact match. Tooltip Controls in the Windows Shell and Controls section.
I started reading, I was surprised to find a considerable amount of information on how to create and show different kinds of tooltips with the same control class. I'll make a brief explanation on what I found and what I understood.
Technically speaking, a balloon tooltip is a window, not a normal window like we all know it, of course, but a window declared from a window class, kind of confusing right? Let's walk step by step.
As I mentioned before, the balloon tooltip generated in this demo is created after calling the CreateWindowEx
WinAPI function.
According to MSDN:
The CreateWindowEx
function creates an overlapped, pop-up, or child window with an extended window style.
As you can see, it receives a lot of parameters:
HWND CreateWindowEx(
DWORD dwExStyle, LPCTSTR lpClassName, LPCTSTR lpWindowName,
DWORD dwStyle, int x, int y, int nWidth,
int nHeight, HWND hWndParent, HMENU hMenu,
HINSTANCE hInstance, LPVOID lpParam
);
----------------
[DllImport("User32", SetLastError=true)]
internal static extern int CreateWindowEx (
int dwExStyle, string lpClassName, string lpWindowName, int dwStyle,
int X, int Y, int nWidth, int nHeight, int hWndParent, int hMenu,
int hInstance, IntPtr lpParam);
----------------
m_lTTHwnd = CreateWindowEx(0, TOOLTIP_CLASS, string.Empty,
lWinStyle, 0, 0, 0, 0, m_lParentHwnd, 0, 0, IntPtr.Zero);
Each parameter has its own description (look in MSDN), only this time we'll focus just in the second one, the LPCTSTR lpClassName
.
According to MSDN description:
lpClassName
must be a Pointer to a null-terminated string or a class atom created by a previous call to the RegisterClass
or RegisterClassEx
function. The atom must be in the low-order word of lpClassName
; the high-order word must be zero. If lpClassName
is a string, it specifies the window class name. The class name can be any name registered with RegisterClass
or RegisterClassEx
, provided that the module that registers the class is also the module that creates the window. The class name can also be any of the predefined system class names.
Forget about the atom part, and read the string part again.... It means that CreateWindowEx
can create handles to different window classes. There are three types of window classes:
- System classes
- Application Global classes
- Application Local classes
These types differ in scope and in when and how they are registered and destroyed. Reading a little more from MSDN, I found out that the Button, the ComboBox, the ListBox and other controls are system classes. This clearly points out that a ToolTip class exists and it's called "TOOLTIPS_CLASS
".
As the CreateWindowEx
function clearly states it, this class must be registered, and it is when the common control dynamic-link library (Comctl32.dll) is loaded.
If the function succeeds, the return value is a handle to the new tooltip. With this unique number, you can communicate to the balloon using window messages and configure it.
The Balloon Show...on the road
The communication takes place thanks to the existence of Windows Procedures. Every window has an associated function that processes all messages sent or posted to all windows of the class. All aspects of a window's appearance and behavior depend on the window procedure's response to these messages, and the balloon tooltip window is no exception.
These messages are normally sent by the system, but the user can use the SendMessage function to call the window procedure for a specified window.
LRESULT SendMessage(
HWND hWnd, UINT Msg,
WPARAM wParam, LPARAM lParam
);
hWnd
- is the handle to the window whose window procedure will receive the message.
Msg
- Specifies the message to be sent.
wParam
- Specifies additional message-specific information.
lParam
- Specifies additional message-specific information.
The return value specifies the outcome of the processed message and depends on the message sent.
All the messages that can be sent to the Tooltip are declared and can be found in "CommCtrl.h". This time, I'll just print out a few of them:
#define TTM_ACTIVATE (WM_USER + 1)
#define TTM_SETDELAYTIME (WM_USER + 3)
#define TTM_ADDTOOLA (WM_USER + 4)
#define TTM_ADDTOOLW (WM_USER + 50)
#define TTM_DELTOOLA (WM_USER + 5)
#define TTM_DELTOOLW (WM_USER + 51)
#define TTM_NEWTOOLRECTA (WM_USER + 6)
#define TTM_NEWTOOLRECTW (WM_USER + 52)
#define TTM_RELAYEVENT (WM_USER + 7)
As SendMessage
function clearly states it, each message has its own parameters, so does each TTMessage
. This project shows you how to use most of them, and even more, it shows you how to work with them using C#.
Disadvantages
I found several reasons why you would not like to use the Balloon tips, and I feel it's only fair you know them.
- Only three kinds of icons can be displayed, the , the and the icons.
- If no title is chosen, no icon is displayed.
- Placing them by code in a particular point in the screen can be a little hard.
- You can't choose the position of the tip, normally it displays upwards. The tip position is calculated when the balloon is displayed. It depends on the balloon-screen's position. I found a way to display the balloon downwards anywhere, using the
MoveWindow
WinAPI function.
Points of Interest
In order to understand well this article and its source, you should have some knowledge in WinAPI functions, constants, and structs. If you are new to all these, there are some articles on CodeProject and a lot of info on the Web. There are two sites I recommend, and the applications they host have been awfully helpful when I've had to develop WinAPI stuff.
Final Words
This article describes only a tip of the balloon tooltip control, the source code provided will help you understand it a lot better, so don't hesitate to download it and learn. There's still some more that can always be done, MSDN has the info and there's a lot of help from users like you and me all around the web, you just have to dive in to the help and the code provided and find your own way out. No one says it's easy or fast, this article (with the project included) took me a lot of time to get running, but at the end, we all won.
Don't forget, any comments, critics, questions, mistakes or anything are always welcomed.