SimpleIsBest.NET

유경상의 닷넷 블로그
이 글은 오래된 전에 작성된 글입니다. 따라서 최신 버전의 기술에 알맞지 않거나 오류를 유발할 수 있습니다. 저자는 이 글에 대한 질문을 받지 않을 것입니다. 하지만 이 글이 리뉴얼 되면 이 글에 대한 질문을 하거나 토론을 할 수도 있습니다.
요즘엔 스마트 클라이언트 덕인지 윈폼을 가지고 노는 경우가 많습니다. 최근에 우연히 살펴본 닷넷 프레임워크 2.0의 윈폼(WinForm)에서 추가된 기능을 살펴보다가 NotifyIcon 클래스에 ShowBalloonTip 메쏘드가 추가된 것을 보고 이것을 1.1에서도 풍선 팁을 사용할 수 있는 방법을 찾아 보았습니다.

Tray Icon Balloon Tip in .NET Framework 1.1

윈도우 95에서 부터 추가된 작업 표시줄(task bar)의 오른쪽에는 시스템 트레이(system tray)라고 하는 영역이 있다. 이 영역에는 시스템 시간을 표시하거나 수행중인 프로그램을 표시하거나, 사용자에게 무엇인가를 알리고자 하는 용도의 다양한 아이콘들이 상주하고 있다. 응용 프로그램이 트레이 영역에 아이콘을 표시하기 위해서는 이 아이콘을 영역에 마우스 클릭 등등의 이벤트를 수신한 윈도우를 지정하고(대개 별도의 윈도우를 만들고 HIDEN으로 해둔다), 이것을 SHELL API를 통해 쉘(SHELL)에 등록해 주면 된다.

NotifyIcon class in .NET Framework

말이 쉽지, 트레이 영역에 아이콘을 추가하는 것은 상당히 귀찮은 작업이다. VB 6.0에서 트레이 아이콘을 추가하려고 하면  족히 200 여 라인은 존나게 코딩 해야만 트레이에 달랑 아이콘 하나를 구경할 수 있다(KB 자료에 친절한 설명과 예제가 나와 있다). 우리의 친절한 닷넷씨는 트레이 아이콘을 위해 NotifyIcon 이란 클래스를 제공한다. 사용법은 종니 간단하다. NotifyIcon을 폼에 추가하고 Visible 속성을 true로 주기만 하면 트레이에 아이콘이 나타난다.

너무 간단한 관계로 자존심이 상해서 예제는 못 올리겠다. -_-; 굳이 예제를 반드시 봐야만 직성이 성격 이상한 풀리겠다는 독자는 CodeProject 나 기타 구글 신에게 물어보기 바란다. 또 잔소리 몇 마디 하자면, 예제만 쳐다봐서는 절대 어느 이상 실력이 늘지 않는다. 지금도 늦지 않았으니 잽싸게 비주얼 스튜디오 띄우고 몇 줄 안 되는 코드 작성해 봐라. WindowApplication198 프로젝트를 만들었다면 당신은 벌써 고수 반열에 올라섰을 것이며 WindowApplication1 이라면 몇 년 안에 다른 직업으로 이직할 가능성이 높은 사람이다.

ShowBalloonTip() Method in .NET Framework 2.0

닷넷 프레임워크 2.0에는 다른 여러 클래스들과 마찬가지로 NotifyIcon 클래스 역시 기능이 확장되었다. 뭐 대단한 건 아니고, Windows XP 부터 지원되는 풍선 도움말 기능을 포함하고 있는 것이다. 화면1과 같은 풍선 도움말을 트레이 아이콘에 표시할 수 있는 메쏘드로서 ShowBalloonTip 메쏘드와 관련 속성으로 BalloonTipText, BalloonTipTitle 등을 갖고 있다. 이들 속성에 적절한 메시지를 넣고 ShowBalloonTop 메쏘드를 호출하기만 하면 화면1과 같은 풍선 도움말을 간단히 구현할 수 있다.


화면1. 트레이 아이콘의 풍선 팁

NotifyIcon & Balloon Top in .NET Framework 1.1

아쉽게도 닷넷 프레임워크 1.1의 NotifyIcon 클래스는 풍선 팁에 대한 어떤 메쏘드나 속성도 가지고 있지 않다(좀 신경 좀 쓰지... 찌질하게... 이런 것도 추가 안해 놓다니... -_-). 물론 우리에겐 막강한 P/Invoke 가 있으니 Shell API를 직접 호출하면 충분히 원하는 풍선 도움말을 우리 어플리케이션에 추가하고 목에 힘을 줄 수 있을 것이다. 트레이 영역에 아이콘을 추가하는 API는 Shell_NotifyIncon() 함수를 사용하면 된다. 이 함수는 달랑 2개의 매개변수 밖에 취하지 않지만, 두 번째 매개변수는 상당한 덩치를 자랑하는 NOTIFYICONDATA 구조체 이다. 뭐 쫄 것까지는 없다. P/Invoke.NET 에서 검색을 해 보던가, 아니면 필자처럼 Reflector를 써서 닷넷 프레임워크의 선언을 훔쳐다 써도 된다.

주) WIN32에서는 NOTIFYICONDATA가 구조체(struct 키워드)로 정의 되었지만, C#에서는 디폴트 생성자를 사용하기 위해 class로 선언되었음에 주의한다. 혹시...  C#의 구조체가 디폴트 생성자를 가질 수 없는 것을 모르는 것은 아니겄지?

    1 // SHELL API의 풍선 툴팁을 위한 NOTIFYICONDATA 구조체

    2 [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Auto)]

    3 public class NOTIFYICONDATA

    4 {

    5     public int cbSize;

    6     public IntPtr hWnd;

    7     public int uID;

    8     public int uFlags;

    9     public int uCallbackMessage;

   10     public IntPtr hIcon;

   11     [MarshalAs(UnmanagedType.ByValTStr, SizeConst=0x80)]

   12     public string szTip;

   13     public int dwState;

   14     public int dwStateMask;

   15     [MarshalAs(UnmanagedType.ByValTStr, SizeConst=0x100)]

   16     public string szInfo;

   17     public int uTimeoutOrVersion;

   18     [MarshalAs(UnmanagedType.ByValTStr, SizeConst=0x40)]

   19     public string szInfoTitle;

   20     public int dwInfoFlags;

   21     public NOTIFYICONDATA()

   22     {

   23         this.cbSize = Marshal.SizeOf(typeof(NOTIFYICONDATA));

   24     }

   25 }

   26 

   27 // NOTIFYICONDATA의 uFlags 관련 상수

   28 private const int NIF_ICON = 0x00000002;

   29 private const int NIF_INFO = 0x00000010;

   30 

   31 // SHELL API NotifyIcon 생성

   32 [DllImport("shell32.dll", CharSet=CharSet.Auto)]

   33 public static extern int Shell_NotifyIcon(int message, NOTIFYICONDATA pnid);

리스트1. Shell_NotifyIcon에 대한 P/Invoke 선언

필자의 선언은 리스트1과 같다. 이제 NOTIFYICONDATA 구조체의 각 필드 값들을 채워 넣고 Shell_NotifyIcon 함수를 호출하기만 하면 풍선 도움말을 얻을 수 있다.

줴~엔~장~ 세상엔 쉬운 게 하나두 없는 것 같다. 만만하게 생각했던 Notify Icon도 복병이 숨어 있었으니, 바로 NOTIFYICONDATA 구조체에 채워야 할 hWnd 필드(라인 6)와 uID 필드(라인 7)가 필자의 심기를 건드린 것이다. 트레이 아이콘, 좀 더 정확한 용어로는 Notify Icon 을 표시하기 위해서는 트레이 아이콘에서 발생하는 마우스 이벤트 등을 처리할 윈도우가 필요하다. 그리고 하나의 어플리케이션(exe)는 여러 트레이 아이콘을 표시할 수 있으므로 이들 트레이 아이콘을 구분할 ID 역시 필요하다. 이 두 값을 NOTIFYICONDATA 구조체의 hWnd 멤버와 uID 멤버에 채워 넣어야 한다. 그렇다면 이 두 값을 어떻게 구해야 하는가?

트레이 아이콘은 NotifyIcon 클래스에 의해 표시되며 이 클래스 역시 용빼는 재주가 없으므로 NOTIFYICONDATA 구조체와 Shell_NotifyIcon 함수를 사용하도록 되어 있다(Reflector로 까보면 금방 알 수 있다). 그리고 트레이 아이콘에 사용하는 윈도우는 Hidden 윈도우로 NotifyIcon 클래스 내부에서 생성하도록 되어 있다. 그리고 트레이 아이콘에 사용된 ID 값 역시 NotifyIcon 클래스 내부에 기록되어 있다(1 부터 순차적인 값이 NotifyIcon 클래스의 인스턴스 마다 할당된다). 그렇다. NotifyIcon 내부에 private로 감추어진 두 멤버의 값을 알아내기만 하면 풍선 도움말을 표시하는데 필요한 값들을 알아낼 수 있는 것이다.

private 멤버의 값을 알아내는 것이 어렵거나 불가능해 보이지만, Reflection을 쓰면 어렵지 않게 알아낼 수 있다. NotifyIcon 클래스의 private 멤버인 window 필드는 NativeWindow 타입으로서 다음과 같은 코드로 그 값과 핸들을 알아 낼 수 있다. 뭐 무슨 사기꾼 같아 보이지만 가능한 건 가능한 것이니까... -_-; 비슷한 코드를 이용하여 트레이 아이콘 ID 역시 알아낼 수도 있다(리스트2).

    1 // NotifyIcon 객체의 윈도우 핸들을 Reflection을 통해 구한다.

    2 private static IntPtr GetNotifyIconHandle(NotifyIcon icon)

    3 {

    4     Type classType = icon.GetType();

    5     System.Reflection.FieldInfo fieldInfo = classType.GetField("window",

    6         System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);

    7     NativeWindow window = (NativeWindow)fieldInfo.GetValue(icon);

    8     return window.Handle;

    9 }

   10 

   11 // NotifyIcon 객체의 아이콘 아이디를 Reflection을 통해 구한다.

   12 private static int GetNotifyIconId(NotifyIcon icon)

   13 {

   14     Type classType = icon.GetType();

   15     System.Reflection.FieldInfo fieldInfo = classType.GetField("id",

   16         System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);

   17     int id = (int)fieldInfo.GetValue(icon);

   18     return id;

   19 }

리스트2. NotifyIcon 클래스에서 Reflection을 이용하여 트레이 아이콘의 윈도우 핸들과 ID를 알아내는 코드

필요한 것을 모두 알아냈으니 이제 Shell_NotifyIcon() 함수를 호출하기만 하면 풍선 도움말을 알아낼 수 있게 되었다. NOTIFYICONDATA 구조체에 필요한 값들을 채워 넣고 Shell_NotifyIcon 함수만 호출하면 되는 매우 간단한 코드이다. NOTIFYICONDATA 구조체의 어떤 필드들을 채워야 하는 가는 NOTIFYICONDATA 구조체에 대한 MSDN 도움말을 참고하면 된다. 이러한 류의 WIN32 API를 호출해 본 경험이 없다면 어렵게 느껴지겠지만, 여러 함수들을 사용하다 보면 익숙해 질 것이며, MSDN 도움말을 읽는 요령 또한 생기리라 믿는다. 혹자는 이렇게 MSDN을 참고하라고 얘기 하면 무성의 하다고 욕하는 이도 있지만, 필자의 생각은 아무런 노력 없이 낼름 낼름 받아먹을 생각만 하는 것 자체가 문제라고 생각하는 바이다(이 점에 대해서는 할말이 많지만 다음 기회에 썰을 풀기로 하고 꾹 참아 볼란다).

    1 public void ShowBalloonTip(NotifyIcon icon, int timeout, string tipTitle, string tipText, ToolTipIcon tipIcon)

    2 {

    3     NOTIFYICONDATA notifyicondata1 = new NOTIFYICONDATA();

    4     notifyicondata1.hWnd = GetNotifyIconHandle(icon);

    5     notifyicondata1.uID = GetNotifyIconId(icon);

    6     notifyicondata1.uFlags = NIF_INFO;

    7     notifyicondata1.uTimeoutOrVersion = timeout;

    8     notifyicondata1.szInfoTitle = tipTitle;

    9     notifyicondata1.szInfo = tipText;

   10     notifyicondata1.dwInfoFlags = (int)tipIcon;

   11     Shell_NotifyIcon(1, notifyicondata1);

   12 }

리스트3. Shell_NotifyIcon 함수를 호출하는 ShowBalloonTip 메쏘드 구현

리스트3의 코드에서 추가로 언급할 부분은 uTimeoutOrVersion 필드로서 NOTIFYICONDATA 구조체에서는 unicon 으로 선언된 필드이다. 풍선 팁 표시를 위해서는 타임 아웃값으로 사용되며, 타임 아웃 값은 msec (밀리초; 천분의 1초) 단위이다. 이 값의 최소값은 10초로서 이 값보다 작은 값은 10초로 설정되고, 최대값은 30초로서 역시 이 값을 초과하는 경우 최대값으로 설정된다. 그리고 ToolTipIcon 타입은 열거자로 필자가 정의한 것으로(.NET Framework 2.0과 동일하게 정의 했다), 풍선 팁의 상단 제목의 왼쪽에 표시되는 아이콘을 지시한다. 이 풍선 팁의 아이콘은 Information, error, warning 중 하나로 설정하거나 현재 트레이 아이콘과 같은 아이콘으로 설정할 수 있다.

    1 public enum ToolTipIcon

    2 {

    3     None = 0,

    4     Info = 1,

    5     Warning = 2,

    6     Error = 3,

    7     User = 4            // WindowsXP SP2 이상에서 지원한다.

    8 }

Consideration

앞서 보여준 코드들과 필자가 설명한 내용을 잘 이해했다면, 위와 같은 코드들을 묶어서 하나의 클래스로 만드는 것이 좋을 것이다. 더욱 더 좋게 하는 것은 NotifyIcon 클래스를 상속받아 NotifyIconEx 클래스를 만들어 사용한다면 쓸만한 윈폼 컴포넌트가 탄생할 것이다. 하지만 짜증이 물밀듯이 밀려오는 시츄에이션은 NotifyIcon 클래스가 sealed 클래스이기 때문에 상속이 불가능하다. 상속이 불가능하기 때문에 죽으나 사나 Helper 클래스를 만들어 사용할 수 밖에 없다. 이 만큼 알려줬으니 필자가 알려준 내용으로 멋진 Helper 클래스를 말아 보는 것은 독자의 몫으로 남기겠다...



Comments (read-only)
#re: 닷넷 프레임워크 1.1을 위한 트레이(Tray) 아이콘 풍선 팁... / 심승일 / 2006-02-09 오전 11:49:00
WindowApplication198 프로젝트 라는게 뭔가요? 어디선가 예제를 직접 하는거 같은데.. 알려주세요~
#re: 닷넷 프레임워크 1.1을 위한 트레이(Tray) 아이콘 풍선 팁... / 블로그쥔장 / 2006-02-09 오후 1:06:00
ㅎㅎㅎ
WindowsApplication198은 이런 의미입니다.
기본적으로 비주얼 스튜디오에서 윈폼 어플리케이션 프로젝트를 만들면 WindowsApplication1 이란 이름이 기본적으로 사용되곤
하지요. 하지만 동일한 이름의 프로젝트가 이미 있다면 WindowsApplication2 가 될 것이구요.
만약 많은 예제 프로그램을 만들었다면, 비주얼 스튜디오가 제시하는 기본 프로젝트 이름이 WindowsApplication198 이 될 수도
있다는 말이고, 그만큼 다양하고 많은 예제를 만들어 봤다는 뜻으로 한 말입니다.

^^
#re: 닷넷 프레임워크 1.1을 위한 트레이(Tray) 아이콘 풍선 팁... / 심승일 / 2006-02-09 오후 1:54:00
아하.. ^^ 전또 다른 어딘가에 강좌같은게 있어서 하나씩 문제를 풀어가는줄 았았죠~
알겠습니다. 답변 감사드립니다.