gbInstanceManager

gbInstanceManager is a demonstration of how to manage instances within a PowerBASIC application. Limiting the number of instances, synchronizing menus, and closing selected instances are a few of the key features. The code does not lend itself to a single include file for incorporating the features into an application, but is modular enough that adding it to an application is quite easy.

Download (v1.0, 40K)

gbInstanceManager is a demonstration of managing instances, so there is no real content to the main screen - just a single toolbar with a button dropdown menu which displays a menu that shows actions and settings a user may make.

Here's the main window, with several instances displayed:

Here's the main window, with the various menu options dislayed.

The dropdown menu is discussed below in the section on the Toolbar.

Feature List

gbInstanceManager provides a variety of features to change how the word list is created, viewed, or modified:

Also, see the Programmers Notes below for information on source code that provides useful features that may not be on this list.


Toolbar

The gbInstanceManager toolbar is used only to provide the Settings button, and its attendant dropdown menu. Other than Help, the toolbar has no other buttons.

   

The following options are available on the toolbar.

Settings
Pressing the Settings button will open a new instance, as constraing by the instance settings. The dropdown arrow to the right of the Setting button is discussed in the next section.

Help
Displays this online help page in the user's default browser client.


Settings DropDown Menu

A dropdown menu, with the following options, is associated with the toolbar Settings button.

Ask
Forces gbInstanceManager to ask the user each time a new instance is opened. Ask/AutoOpen/AutoSwitch are mutually exclusive settings.

AutoOpen
Opens a new instance, up to the maximum number of instances allowed, without asking the user for approval. Ask/AutoOpen/AutoSwitch are mutually exclusive settings.

AutoSwitch
Prevents the user from opening a new instance. Instead, displays and gives focus to the most recent instance. Ask/AutoOpen/AutoSwitch are mutually exclusive settings.

TimeStamp Instances
Adds, to the caption, the time an instance was started. Changing this setting changes all existing/future instances.

Close All
Closes all instances, include the instance in which the command was selected.

Close All Other
Closes all instances, except for the instance in which the command was selected.

Open New Instance
Attempts to open new instance, subject to the settings above. This is equivalent to double-clicking the application shortcut or double-clicking on the application in Windows Explorer.

Set Max Instance Count
By default, only 5 instances are allowed by gbInstanceManager. Use this option to change the default.


Programmer Notes

Here are some comments about the code, pointing out the reason why some of the code is written as it is. Code snippets are shown for reference, but please refer to the complete source code listing to get the full context of the examples.

Existence of Previous Instances
Identifying whether a previous instance of an application exists is done using mutexes. This line of code shows the basic approach:

   If CreateMutex(ByVal %Null, 0, UniqueName) = 0 Or _
      GetLastError = %ERROR_ALREADY_EXISTS Then 

See the AllowNetInstance procedure for more details.

Identifying Which WIndows are Instances
With SDK window creation, the class name of the windows can be set by the code - making it easy to find instances of an application. But since all DDT dialogs carry the same #32770 class name, a different mechanism is needed locate all instances of an application.

gbInstanceManager takes the approach of using SetWindowLong() to assign an arbitrary LONG number to all instances. With 4G numbers to choose from, the odds of another app also assigning the same user value is acceptably low.

Here are the lines of code that support the assignment:

   %UniqueNumber = 1818181818
   SetWindowLong(hDlg, %GWL_USERDATA, %UniqueNumber)

Synchronizing Menu Options
gbInstanceManager shows how to synchronize menu settings, in real-time, across all instances. The approach taken is to register a custom Windows message and use that message to send settings information to all instances. Instances which receive the message will update their menu settings to match.

Here are the lines of code that support registering the custom message:

   $CustomMsg = "InstanceSetting" 
   hMsg = RegisterWindowMessage($CustomMsg)

Custom messages, like any message, allows sending of two LONG values. The comments in the code below describe the values sent with the custom message. The Select Case statements should the actual code that is run when a custom message is received. See the dialog callback function for more details.

   Case hMsg
      '0,hWin     hWin is the sending instance
      '1,n        n is maxinstancecount
      '2,n        n is instance setting
      Select Case Cb.WParam
         Case 0    : If hDlg <> Cb.LParam Then Dialog End hDlg
         Case 1    : MaxInstanceCount = Cb.LParam
         Case 2    : InstanceSetting = Cb.LParam  : SetMenuStatus
         Case 3    : TimeStamp = Cb.LParam        : SetMenuStatus
      End Select 

Session Information
The location and size of the last opened instance is saved in an INI file. When a new instance is created, it reads the INI file and opens in a staggered position from the last instance.

These two lines of code are used to save/restore dialog location and size. They both call the Settings_INI procedure, which serves to save and restore dialog location and size.

   Settings_INI "get"
   Settings_INI "save"

The "get" of location/size information is done only once - when the instance is started.

The "save" of location/size information, for the active instance, occurs with three events:

- Close All Other
- New Instance
- WM_Destroy

See the Settings_INI procedure for more details.

Options Management
The INI file is also used to store the Ask/AutoOpen/AutoSwitch and TimeStamp settings. Because it is necessary to save these settings at the moment an instance changes the settings (so the instance can synchronize its menu settings), the Settings_INI "get"/"save" code is not used. Two custom procedures, GetInstanceSettings and SaveInstanceSettings are used instead. These only modify the INI to update the properties just mentioned.

GetInstanceSettings is used once during a session, when creating a new instance.

SaveInstanceSettings is used each time the Ask/AutoOpen/AutoSwitch or TimeStamp settings are changed in an instance. That makes the values available to future instances.

Identifying Most Recent Instance
To identify the most recent instance handle (opened when AutoSwitch or Ask/No are used) a loop is used with GetWindow() and GetWindowLong() to identify windows whose user data match the unique number (1818181818 is used in gbInstanceManager). When a matching window is found, the GetWindowThreadProcessID() and OpenProcess() are used to get the creation time of the application. The CompareFileTime() API is used to identify the most recent instance creation time for use in identifying the most recent instance.

   'get the most recent matching instance, limit # instances
    hTry = GetForegroundWindow()
    Do While hTry
       If GetWindowLong(hTry, %GWL_UserData) = %UniqueNumber Then
         Incr InstanceCount
         If InstanceCount >= MaxInstanceCount Then
            ghHook = SetWindowsHookEx(%WH_CBT, CodePtr(SBProc), GetModuleHandle(""), GetCurrentThreadId)
            MsgBox "No more instances allowed.", %MB_Ok + %MB_IconInformation, "Open New Instance"
            Beep : Function = 0 : Exit Function  '<--- # instances exceeds max count
         End If
         'get hWin creation time
         GetWindowThreadProcessID(hTry,PID)
         hProcess = OpenProcess(%Process_Query_Information Or %Process_VM_Read, %False, PID)
         If hProcess Then
             GetProcessTimes(hProcess, CreationTime, ExitTime, KernelTime, UserTime)
             If CompareFileTime(CreationTime, MostRecentFT) = 1 Then
                MostRecentFT = CreationTime
                hMostRecent = hTry   'later, focus will be given to this instance
             End If
         End If
      End If
      hTry = GetWindow(hTry, %GW_HWNDNEXT)
    Loop 

Small InputBox$
The problem with a standard input box it that it is so ugly! The programmer has little control over what it looks like. But with the following code, you can make it look much nicer, like this:

     

Call InputBox$ like this:

   ghHook = SetWindowsHookEx(%WH_CBT, CodePtr(InputBoxProcExt), GetModuleHandle(""), GetCurrentThreadId)
   temp$ = InputBox$("", "Max Instance Count", Str$(MaxInstanceCount),w+40,h+150)

with this supporting procedure:

Function InputBoxProcExt(ByVal nCode As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
   Local szTemp As WStringZ * %Max_Path, cw As CBT_CREATEWND Ptr, cst As CREATESTRUCT Ptr
   Function = CallNextHookEx(ByVal ghHook, ByVal nCode, ByVal wParam, ByVal lParam)
   If nCode < 0 Then Exit Function
   If nCode = %HCBT_ACTIVATE Then UnhookWindowsHookEx ghHook
   If nCode = %HCBT_CREATEWND Then
      cw = lParam         ' Get pointer to CBT_CREATEWND struct so we can...    TT: Nick Melnick
      cst = @cw.lpcs      ' get a pointer to the CREATESTRUCT struct
      GetClassName wParam, szTemp, %Max_Path      ' for each window / control as it is created
'      If UCase$(szTemp) = "BUTTON" Then @cst.cx = @cst20                           'prompt
      If UCase$(szTemp) = "BUTTON" Then @cst.x = 75                           'ok and cancel
      If UCase$(szTemp) = "STATIC" Then @cst.y = -50 : @cst.cx = 0  : @cst.cy = 20           'prompt
      If UCase$(szTemp) = "EDIT"   Then @cst.cx = 50 : @cst.y = @cst.y - 140  'textbox
      If UCase$(szTemp) = "EDIT"   Then SetWindowLong wparam, %GWL_Style, GetWindowLong(wParam,%GWL_Style)
      If UCase$(szTemp) = "#32770" Then @cst.cx = 175 : @cst.cy = @cst.cy - 130                'dialog
   End If
End Function 

Dynamic menu caption (shows max instances setting in menu)
When there is a numerical value to be set, I like the numerical value to be visible in the menu string. Just place a Menu Set Text line of code before the popup menu is called, like this:

   Case %WM_Notify
      nmtb = Cb.LParam
      Select Case @nmtb.hdr.Code
         Case %TBN_DROPDOWN
            Select Case @nmtb.iItem
               Case %IDT_Settings
                  Menu Set Text hSettings, ByCmd %IDM_MaxInstanceCount, _
                      "Set Max Instance Count   ("  + LTrim$(Str$(MaxInstanceCount)) + ")"
                  Call SendMessage(@nmtb.hdr.hwndFrom, %TB_GETRECT, @nmtb.iItem, VarPtr(rc))
                  Call MapWindowPoints(@nmtb.hdr.hwndFrom, %HWND_Desktop, ByVal VarPtr(rc), 2)
                  Call TrackPopupMenu (hSettings, 0, rc.nLeft, rc.nBottom, 0, CbHndl, ByVal %NULL)
            End Select
      End Select

Locate MsgBox to point of use
The standard MsgBox$ does not allow you to place its window in a specified position. But with hooking you can put it anywhere you want. You can center it in the parent dialog or simply place it where the mouse clicked the menu option, which is what I do in gbInstanceManager.

Call the MsgBox$ with code like this:

   ghHook = SetWindowsHookEx(%WH_CBT, CodePtr(SBProc), GetModuleHandle(""), GetCurrentThreadId)
   If MsgBox ("gbInstance already open. Open new instance?", %MB_OkCancel + _
          %MB_IconInformation, "Previous Instance Check") = %IdCancel Then    

and use this supporting procedure:

Function SBProc(ByVal lMsg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
   Local pt As Point
   GetCursorPos pt
   If lMsg = %HCBT_ACTIVATE Then
      SetWindowPos wParam, 0, pt.x+25, pt.y+25, 0, 0, %SWP_NOSIZE Or %SWP_NOACTIVATE Or %SWP_NOZORDER
      UnhookWindowsHookEx ghHook
   End If
End Function


Other Comments

Misecellaneous information is provided in this section.

INI File
gbInstanceManager application settings are saved in an INI file, kept in the same folder as the gbInstanceManager application.

Comments and suggestions are welcome!