SDI/MDI MFC Application in the Windows System Tray

Environment: VC6, VC7, Windows 9x/NT/2000/XP

Overview

This article describes the process of creating an application that is startup-hidden and is opened when the user clicks the System Tray icon. The SDI VC++ v6.0 demo project is included. The project demonstrates the use of a hidden window, a System Tray icon, and a startup flipping prevent technique.

Introduction

The right part of the Windows Task Bar is the System Tray. It includes icons of hidden applications such as the system clock dialog, the sound volume dialog, some antivirus programs, and so forth.

I created an SDI/MFC project to demonstrate the technique of creating such an application. Why SDI? I needed an application with a graphic output (see http://www.brigsoft.com/bsactivity). A dialog-based application could be created the same way. It is easier because dialog applications don't have the flipping problem that is described below.

Icon Control

Do you want to put an icon of your program in the System Tray? No problem. The SDK Shell_NotifyIcon function lets you put, remove, or change an icon in the System Tray. I created a CMainFrame::TrayMessage(DWORD dwMessage) method for the convenient use of this function. The dwMessage parameter defines what we need to do with the System Tray icon.

BOOL CMainFrame::TrayMessage( DWORD dwMessage)
  {

  CString sTip(_T("BrigSoft Example"));
  NOTIFYICONDATA tnd;
  tnd.cbSize = sizeof(NOTIFYICONDATA);
  tnd.hWnd = m_hWnd;
  tnd.uID = IDR_TRAYICON;
  tnd.uFlags = NIF_MESSAGE|NIF_ICON;
  tnd.uCallbackMessage = MYWM_NOTIFYICON;
  tnd.uFlags = NIF_MESSAGE|NIF_ICON|NIF_TIP; 
  VERIFY( tnd.hIcon = LoadIcon(AfxGetInstanceHandle(),
                               MAKEINTRESOURCE (IDR_TRAYICON)) );
  lstrcpyn(tnd.szTip, (LPCTSTR)sTip, sizeof(tnd.szTip));

  return Shell_NotifyIcon(dwMessage, &tnd);

  }

I call TrayMessage(NIM_ADD) from CMainFrame::OnCreate. It shows an icon after the program is started. Calling TrayMessage( NIM_DELETE ) from CMainFrame::OnClose removes the icon when the application is finished.

You can easily change this function to use the NIM_MODIFY message if you want to change the icon or the ToolTip.

Show and Hide the Main Window

The Shell_NotifyIcon function receives the NOTIFYICONDATA structure. It includes a handle of a window and a window message number. If some event takes place on the icon, the System Tray sends the message to the window. I defined user message MYWM_NOTIFYICON for this purpose.

#define MYWM_NOTIFYICON (WM_USER+2)

To receive this message, I overloaded the virtual function WindowProc (using MFC Wizard).

LRESULT CMainFrame::WindowProc(UINT message, WPARAM wParam,
                                             LPARAM lParam)
{

    // Open window when the user double-clicks the Systray Icon
    if(message == MYWM_NOTIFYICON){

      switch (lParam){

        case WM_LBUTTONDBLCLK:

          switch (wParam) {

            case IDR_TRAYICON:

              ShowWindow(SW_NORMAL);
              SetForegroundWindow();
              SetFocus();
              return TRUE;

            break;

          }

      break;

    }

  }

  return CFrameWnd::WindowProc(message, wParam, lParam);

}

I catch only the WM_LBUTTONDBLCLK message. Other mouse messages can be caught, too. After the WM_LBUTTONDBLCLK occurred, I open the window by using ShowWindow(SW_NORMAL).

Most of the System Tray programs hide their main window instead of minimizing it. To do so, I use the OnSize message handle in this manner:

void CMainFrame::OnSize(UINT nType, int cx, int cy)
{
  if(nType == SIZE_MINIMIZED){
    ShowWindow(SW_HIDE);
  }
  else{
    CFrameWnd::OnSize(nType, cx, cy);
  }
}

Hide on Start

MFC SDI/MDI projects show the main applications window immediately after they start. If you want to start an application in a hidden mode, you must change the InitInstanse() function of the App class. Change the strings:

  m_pMainWnd->ShowWindow(SW_SHOW);
  m_pMainWnd->UpdateWindow();

to

  m_pMainWnd->ShowWindow(SW_HIDE);

The application will start as hidden but you will see a flipping window. It opens and immediately closes. The problem occurs in Microsoft's CDocTemplate class. There is only one solution: to overload the CSingleDocTemplate (CMultiDocTemplate) class. I created the BsDocTemplate class (child of CSingleDocTemplate) to fix this problem. Microsoft can solve this problem easier by changing:

  virtual CDocument* OpenDocumentFile(LPCTSTR lpszPathName,
                                      BOOL bMakeVisible = TRUE);

to

  virtual CDocument* OpenDocumentFile(LPCTSTR lpszPathName,
                                      BOOL bMakeVisible = FALSE);

in the CDocTemplate class definition. But they have not fixed it yet.

Demo Project

The MFCStartUp demo project is a SDI/MFC application. It hides while you start it. So, you will see no windows after the start, only a small green icon in the System Tray. To open the application, double-click this icon. To hide the application, click the minimize button of the main window.

Links

The technique described in this article is used in the project Activity Counter, http://www.brigsoft.com/bsactivity.

© Alex Rest, 2003

Downloads

Download source - 22 Kb


Comments

  • How to keep WinXP from hiding your inactive icon?

    Posted by dlanders on 12/22/2004 01:51pm

    Does anyone know if there is an official mechanism (or have unofficial ideas) to keep Windows XP from "hiding" your inactive tray icon? I assume that by updating the icon periodically, I can keep it "active", but that seems inefficient. Thanks, Dave

    Reply
  • Good job - I have added a popup menu to the tray icon (formatted)

    Posted by macgowan on 03/04/2004 04:57pm

    To add the popup menu to the icon you will need to create a popum menu resource. Go to the resource tab on the ide and create a new menu resource.  We have called ours IDR_MENU_POPUP. 
    
    You will need to handle the WM_RBUTTONDOWN message in the WindowProc() method.  When the user pressed the right mouse button we will call the TrayMenu() method.  
    
    The TrayMenu() method will create the menu and then handle the messages for the menu resource.  
    
    
    
    
    LRESULT CMainFrame::WindowProc(UINT message, WPARAM wParam, LPARAM lParam) 
    {
        // Open window when double click to the Systray Icon
        if(message == MYWM_NOTIFYICON)
        {
                                
            switch (lParam)
            {
    
                case WM_LBUTTONDBLCLK:
                    switch (wParam) 
                    {
                        case IDR_TRAYICON:
                               
                            ShowWindow(SW_NORMAL);
                            SetForegroundWindow();
                            SetFocus();
                            return TRUE;
                            break;
                    }
                    break;
    
                case WM_RBUTTONDOWN:
                    switch (wParam) 
                    {
                        case IDR_TRAYICON:
                                   
                            Beep(1000,100); 
                            TrayMenu(); 
                            break;
                    }
                break;
            }
       }       
            
            return CFrameWnd::WindowProc(message, wParam, lParam);
    }
    
    
    
    
    void CMainFrame::TrayMenu(void)
    {
    
        BOOL    bResult = FALSE;
        DWORD   SelectionMade;
    
        CMenu menu;
    
        CString csMessage; 
    
        int nIndex = 0;
        
        long nRecordNumber = 0;
        long nAbsolutePosition = 0;
    
    
    
        bResult = menu.LoadMenu(IDR_MENU_POPUP);
            
        CMenu* popup = menu.GetSubMenu(0);
            
            
            // menuRtClick.EnableMenuItem(ID_ITEM0, TRUE);
            // menuRtClick.EnableMenuItem(ID_ITEM1, TRUE);
            // menuRtClick.EnableMenuItem(ID_ITEM2, TRUE);
            
            
        // call the helper function to setup this as a titled popup menu
        // AddMenuTitle(popup);
            
            
        POINT pp;
            
            
        GetCursorPos(&pp);
        SelectionMade = popup->TrackPopupMenu(
                               TPM_LEFTALIGN | 
                               TPM_LEFTBUTTON | 
                               TPM_RIGHTBUTTON | 
                               TPM_NONOTIFY | 
                               TPM_RETURNCMD,
                               pp.x,pp.y,this);
            
        popup->DestroyMenu();
            
        // The value of SelectionMade is the id of the command selected or 0 if no 
        // selection was made
            
        switch(SelectionMade)
        {
            case ID_POPUP_START_SERVER:
                AfxMessageBox("Starting Server");
                break;
    
            case ID_POPUP_STOP_SERVER:
                AfxMessageBox("Stopping Server");
                break;
        }
    
        // display the popup menu
        // popup->TrackPopupMenu(TPM_LEFTALIGN, point.x, point.y, this);
            
            // *pResult = 0;
    }

    • system tray menu does not disappear after i cLick outside of the menu

      Posted by savan on 10/19/2004 08:35am

      You should call SetForegroundWindow() method of your dialog, just before you call TrackPopupMenu. That will do the trick.

      Reply
    • system tray menu does not disappear after i cLick outside of the menu

      Posted by rayjeremya on 04/21/2004 03:52am

      by the way, my appLication is diaLog based..

      Reply
    • system tray menu does not disappear after i cLick outside of the menu

      Posted by rayjeremya on 04/21/2004 03:41am

      is there a way to resoLve this?!!!

      Reply
    Reply
  • Good job - I have added the menu to the tray icon

    Posted by macgowan on 03/04/2004 04:55pm

    To add the popup menu to the icon you will need to create a popum menu resource. Go to the resource tab on the ide and create a new menu resource. We have called ours IDR_MENU_POPUP. You will need to handle the WM_RBUTTONDOWN message in the WindowProc() method. When the user pressed the right mouse button we will call the TrayMenu() method. The TrayMenu() method will create the menu and then handle the messages for the menu resource. LRESULT CMainFrame::WindowProc(UINT message, WPARAM wParam, LPARAM lParam) { // Open window when double click to the Systray Icon if(message == MYWM_NOTIFYICON) { switch (lParam) { case WM_LBUTTONDBLCLK: switch (wParam) { case IDR_TRAYICON: ShowWindow(SW_NORMAL); SetForegroundWindow(); SetFocus(); return TRUE; break; } break; case WM_RBUTTONDOWN: switch (wParam) { case IDR_TRAYICON: Beep(1000,100); TrayMenu(); break; } break; } } return CFrameWnd::WindowProc(message, wParam, lParam); } void CMainFrame::TrayMenu(void) { BOOL bResult = FALSE; DWORD SelectionMade; CMenu menu; CString csMessage; int nIndex = 0; long nRecordNumber = 0; long nAbsolutePosition = 0; bResult = menu.LoadMenu(IDR_MENU_POPUP); CMenu* popup = menu.GetSubMenu(0); // menuRtClick.EnableMenuItem(ID_ITEM0, TRUE); // menuRtClick.EnableMenuItem(ID_ITEM1, TRUE); // menuRtClick.EnableMenuItem(ID_ITEM2, TRUE); // call the helper function to setup this as a titled popup menu // AddMenuTitle(popup); POINT pp; GetCursorPos(&pp); SelectionMade = popup->TrackPopupMenu( TPM_LEFTALIGN | TPM_LEFTBUTTON | TPM_RIGHTBUTTON | TPM_NONOTIFY | TPM_RETURNCMD, pp.x,pp.y,this); popup->DestroyMenu(); // The value of SelectionMade is the id of the command selected or 0 if no // selection was made switch(SelectionMade) { case ID_POPUP_START_SERVER: AfxMessageBox("Starting Server"); break; case ID_POPUP_STOP_SERVER: AfxMessageBox("Stopping Server"); break; } // display the popup menu // popup->TrackPopupMenu(TPM_LEFTALIGN, point.x, point.y, this); // *pResult = 0; }

    Reply
  • Assert

    Posted by Legacy on 10/31/2003 12:00am

    Originally posted by: Bowler

    When modifying the View by adding a Dialog-Resource, I'm getting an Assert in Debug-Mode from the BsDocTemplate.

    Can anyone help me please.

    THX

    Bowler

    Reply
  • NNNNNice, by the way...

    Posted by Legacy on 10/26/2003 12:00am

    Originally posted by: -LLayr

    At present I multiple instance explorer to minimize effects of what I thought were either active desktop or system tray crashes. I allow only hardware related apps to instance in the system tray, and only those that I use regularly. At shutdown 50% of the time I recieve a notification that windows is trying to close a program. Instead of selecting ok, I choose cancel. This preserves my desktop settings so that I don't get that annoying restore desktop document at reboot, it also minimizes scandisks. I suspected that what was really occuring was a cache write. However...when I click cancel, I get a PARTIAL tray crash...I lose the volume control icon! After reading your article and code I now suspect it's a bad implementation as you describe. I do not use an exit windows wave, so the mixer is inactive. Aside from changing the default display settings for the icon, can you point me to some code or a routine that would fix these micro-crashes? thanx in adv.

    Reply
  • Help me PlZZZ??

    Posted by Legacy on 10/01/2003 12:00am

    Originally posted by: Himanshu

    Hi all,

    I am using an application where the applicaiton starts with minimized to system tray. I have few options on right click menu to show some dialog. When i package it, and install the application i want to show the dialog open if the user clicks on the icon of application in program files if the tray icon is already added to system tray..can anyone help me out in doing this..any help or pointers are highly appreciated..thanks a lot in advance..
    regards,
    Himanshu

    Reply
  • Adding a popup menu to the icon in the system tray

    Posted by Legacy on 08/31/2003 12:00am

    Originally posted by: Ryan

    Now that we already have an icon in the system tray, I wanted to add a popup menu when I do i right click on the icon.
    
    

    I added below code on the WindowProc(), but it caused an error during run time.

    LRESULT CMyDlg::WindowProc(...)
    {
    switch (message)
    {
    case WM_NOTIFY_TRAY_ICON:
    switch (lParam)
    {
    case WM_RBUTTONDOWN:
    CMenu menu;
    menu.LoadMenu(IDR_ML_MENU);
    CMenu* pPopup = menu.GetSubMenu(0);
    CPoint point;
    GetCursorPos(&point);
    pPopup->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, point.x, point.y, AfxGetMainWnd(), NULL);
    }
    break;
    }
    return CDialog::WindowProc(message, wParam, lParam);
    }

    Do you how to do a work around on this problem?

    Reply
  • Here is an example on how to do the ame thing for Dialog applications

    Posted by Legacy on 06/19/2003 12:00am

    Originally posted by: Chris

    I saw artical and thought it was great.  It only took a few minutes to adapt it to work with an dialog application.
    
    

    I will not rehash this artical, but to point out a few things. When adding new handlers (ie OnSize) use the function calls which are in the created routine rather than what is listed in this artical - for obvious reasons.

    SDI application
    CFrameWnd::OnSize(nType, cx, cy);

    Dialog application
    CDialog::OnSize(nType, cx, cy);

    The only thing that wasn't immediately obvious was how to hide the dialog application on startup. It seems that the MFC framework will not allow a dialog application to be hidden on startup, but it will allow it to be minimized.

    If you try to hide this in the OnInitDialog function, the command will be ignored and the dialog will be simply displayed.

    But the code given in the artical changes minimise messages to hide messages, thus without a bit of changing to OnSize even the normal minimize will fail to work as it is being translated to a hide.

    What I did was to modify the OnSize to simply do a minimise for the first minimise message that comes through, and a hide for all other minimise messages.

    See below.

    void CNewpaperDlg::OnSize(UINT nType, int cx, int cy)
    {
    static first = true;

    if(nType == SIZE_MINIMIZED)
    {
    if (first)
    {
    ShowWindow(SW_MINIMIZE);
    first = false;
    }
    else
    ShowWindow(SW_HIDE);
    }
    else
    {
    CDialog::OnSize(nType, cx, cy);
    }
    }

    This works just fine but when the application initially starts up, it will be displayed on the task bar. The trick is to setup a timer and when the timer goes off, it should then hide the dialog (ie ShowWindow(SW_HIDE) )

    Just add this line to the OnInitDialog function and use the wizard to catch WM_TIMER messages

    SetTimer(998, 1,NULL);

    void CNewpaperDlg::OnTimer(UINT nIDEvent)
    {
    if (nIDEvent == 998)
    {
    ShowWindow(SW_HIDE);
    KillTimer(998);
    }

    CDialog::OnTimer(nIDEvent);
    }

    In theory the user could see the dialog show up on the taskbar, but it all seems to happen so quickly that windows doesn't have time to repaint this.

    Well, this assumes that this program is set to run on startup. There is a small flicker on the taskbar if the application is run from a command prompt.

    Reply
  • How to do the same with Dialog based application?

    Posted by Legacy on 06/17/2003 12:00am

    Originally posted by: VGopal

    First I would like to appreciate
    your good work. Here, I want to know
    how to implement the same with the
    Dialog based application.

    thanking you
    gopal

    Reply
  • Thanks

    Posted by Legacy on 05/22/2003 12:00am

    Originally posted by: Thanh Do

    What a good work!
    Thanks

    Reply
Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • With JRebel, developers get to see their code changes immediately, fine-tune their code with incremental changes, debug, explore and deploy their code with ease (both locally and remotely), and ultimately spend more time coding instead of waiting for the dreaded application redeploy to finish. Every time a developer tests a code change it takes minutes to build and deploy the application. JRebel keeps the app server running at all times, so testing is instantaneous and interactive.

  • The impact of a data loss event can be significant. Real-time data is essential to remaining competitive. Many companies can no longer afford to rely on a truck arriving each day to take backup tapes offsite. For most companies, a cloud backup and recovery solution will eliminate, or significantly reduce, IT resources related to the mundane task of backup and allow your resources to be redeployed to more strategic projects. The cloud - can now be comfortable for you – with 100% recovery from anywhere all …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds